From f037feaebc219ac63d9a92a0ec13b6a580e18a68 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Aug 2020 01:49:52 +0200 Subject: [PATCH 001/862] Handle non-existing translations in clean script (#38574) --- script/translations/clean.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/script/translations/clean.py b/script/translations/clean.py index 1c0178d4c0d..0eabf6214ae 100644 --- a/script/translations/clean.py +++ b/script/translations/clean.py @@ -44,7 +44,10 @@ def find_core(): translations = int_dir / "translations" / "en.json" strings_json = json.loads(strings.read_text()) - translations_json = json.loads(translations.read_text()) + if translations.is_file(): + translations_json = json.loads(translations.read_text()) + else: + translations_json = {} find_extra( strings_json, translations_json, f"component::{int_dir.name}", missing_keys From e32a57ce48c3ac778a5254bf244912081d9d654a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 6 Aug 2020 00:02:06 +0000 Subject: [PATCH 002/862] [ci skip] Translation update --- .../accuweather/translations/sensor.ca.json | 9 ++++++ .../components/adguard/translations/bg.json | 2 -- .../components/adguard/translations/ca.json | 2 -- .../components/adguard/translations/da.json | 2 -- .../components/adguard/translations/de.json | 2 -- .../components/adguard/translations/en.json | 2 -- .../adguard/translations/es-419.json | 2 -- .../components/adguard/translations/es.json | 2 -- .../components/adguard/translations/fr.json | 2 -- .../components/adguard/translations/it.json | 2 -- .../components/adguard/translations/ko.json | 2 -- .../components/adguard/translations/lb.json | 2 -- .../components/adguard/translations/nl.json | 2 -- .../components/adguard/translations/no.json | 2 -- .../components/adguard/translations/pl.json | 2 -- .../components/adguard/translations/ru.json | 2 -- .../components/adguard/translations/sl.json | 2 -- .../components/adguard/translations/sv.json | 2 -- .../adguard/translations/zh-Hant.json | 2 -- .../components/blink/translations/ca.json | 2 ++ .../components/blink/translations/en.json | 4 ++- .../components/blink/translations/no.json | 4 ++- .../components/blink/translations/ru.json | 4 ++- .../components/bond/translations/ca.json | 7 +++++ .../components/harmony/translations/ca.json | 1 - .../components/harmony/translations/en.json | 1 - .../components/harmony/translations/es.json | 1 - .../components/harmony/translations/fr.json | 1 - .../components/harmony/translations/it.json | 1 - .../components/harmony/translations/ko.json | 1 - .../components/harmony/translations/no.json | 1 - .../components/harmony/translations/pl.json | 1 - .../components/harmony/translations/ru.json | 1 - .../harmony/translations/zh-Hant.json | 1 - .../components/hlk_sw16/translations/ca.json | 21 +++++++++++++ .../components/homekit/translations/ca.json | 3 +- .../components/homekit/translations/en.json | 3 +- .../homekit/translations/es-419.json | 3 +- .../components/homekit/translations/es.json | 3 +- .../components/homekit/translations/fr.json | 3 +- .../components/homekit/translations/it.json | 3 +- .../components/homekit/translations/ko.json | 3 +- .../components/homekit/translations/lb.json | 3 +- .../components/homekit/translations/no.json | 3 +- .../components/homekit/translations/ru.json | 3 +- .../components/homekit/translations/sl.json | 3 +- .../homekit/translations/zh-Hans.json | 3 +- .../homekit/translations/zh-Hant.json | 3 +- .../components/hue/translations/ca.json | 1 - .../components/hue/translations/de.json | 1 - .../components/hue/translations/en.json | 1 - .../components/hue/translations/es.json | 1 - .../components/hue/translations/fr.json | 1 - .../components/hue/translations/it.json | 1 - .../components/hue/translations/ko.json | 1 - .../components/hue/translations/lb.json | 1 - .../components/hue/translations/no.json | 1 - .../components/hue/translations/pl.json | 1 - .../components/hue/translations/ru.json | 1 - .../components/hue/translations/zh-Hant.json | 1 - .../components/iqvia/translations/bg.json | 1 - .../components/iqvia/translations/ca.json | 4 ++- .../components/iqvia/translations/da.json | 1 - .../components/iqvia/translations/de.json | 1 - .../components/iqvia/translations/en.json | 1 - .../components/iqvia/translations/es-419.json | 1 - .../components/iqvia/translations/es.json | 1 - .../components/iqvia/translations/fi.json | 1 - .../components/iqvia/translations/fr.json | 1 - .../components/iqvia/translations/it.json | 1 - .../components/iqvia/translations/ko.json | 1 - .../components/iqvia/translations/lb.json | 1 - .../components/iqvia/translations/nl.json | 1 - .../components/iqvia/translations/no.json | 1 - .../components/iqvia/translations/pl.json | 1 - .../components/iqvia/translations/pt-BR.json | 1 - .../components/iqvia/translations/ru.json | 1 - .../components/iqvia/translations/sl.json | 1 - .../components/iqvia/translations/sv.json | 1 - .../iqvia/translations/zh-Hans.json | 1 - .../iqvia/translations/zh-Hant.json | 1 - .../meteo_france/translations/ca.json | 19 ++++++++++++ .../minecraft_server/translations/ca.json | 2 +- .../components/mqtt/translations/ca.json | 2 ++ .../components/netatmo/translations/ca.json | 4 +-- .../components/netatmo/translations/cs.json | 1 - .../components/netatmo/translations/da.json | 3 -- .../components/netatmo/translations/de.json | 1 - .../components/netatmo/translations/en.json | 1 - .../netatmo/translations/es-419.json | 3 -- .../components/netatmo/translations/es.json | 4 +-- .../components/netatmo/translations/fr.json | 1 - .../components/netatmo/translations/hu.json | 1 - .../components/netatmo/translations/it.json | 1 - .../components/netatmo/translations/ko.json | 1 - .../components/netatmo/translations/lb.json | 1 - .../components/netatmo/translations/nl.json | 3 -- .../components/netatmo/translations/no.json | 4 +-- .../components/netatmo/translations/pl.json | 1 - .../components/netatmo/translations/ru.json | 4 +-- .../components/netatmo/translations/sl.json | 1 - .../components/netatmo/translations/sv.json | 3 -- .../netatmo/translations/zh-Hant.json | 1 - .../ovo_energy/translations/ca.json | 18 +++++++++++ .../ovo_energy/translations/en.json | 30 +++++++++---------- .../ovo_energy/translations/no.json | 18 +++++++++++ .../ovo_energy/translations/ru.json | 18 +++++++++++ .../components/owntracks/translations/de.json | 2 +- .../components/pi_hole/translations/ca.json | 3 +- .../components/pi_hole/translations/de.json | 3 +- .../components/pi_hole/translations/en.json | 3 +- .../components/pi_hole/translations/es.json | 3 +- .../components/pi_hole/translations/fr.json | 3 +- .../components/pi_hole/translations/it.json | 3 +- .../components/pi_hole/translations/ko.json | 3 +- .../components/pi_hole/translations/lb.json | 3 +- .../components/pi_hole/translations/nl.json | 3 +- .../components/pi_hole/translations/no.json | 3 +- .../components/pi_hole/translations/pl.json | 3 +- .../pi_hole/translations/pt-BR.json | 3 +- .../components/pi_hole/translations/ru.json | 3 +- .../pi_hole/translations/zh-Hant.json | 3 +- .../components/plex/translations/bg.json | 2 -- .../components/plex/translations/ca.json | 2 -- .../components/plex/translations/da.json | 2 -- .../components/plex/translations/de.json | 2 -- .../components/plex/translations/en.json | 2 -- .../components/plex/translations/es-419.json | 2 -- .../components/plex/translations/es.json | 2 -- .../components/plex/translations/fr.json | 2 -- .../components/plex/translations/hu.json | 2 -- .../components/plex/translations/it.json | 2 -- .../components/plex/translations/ko.json | 2 -- .../components/plex/translations/lb.json | 2 -- .../components/plex/translations/nl.json | 2 -- .../components/plex/translations/no.json | 2 -- .../components/plex/translations/pl.json | 2 -- .../components/plex/translations/pt-BR.json | 3 -- .../components/plex/translations/pt.json | 1 - .../components/plex/translations/ru.json | 2 -- .../components/plex/translations/sl.json | 2 -- .../components/plex/translations/sv.json | 2 -- .../components/plex/translations/zh-Hant.json | 2 -- .../components/poolsense/translations/ca.json | 4 +-- .../components/poolsense/translations/de.json | 4 --- .../components/poolsense/translations/en.json | 4 +-- .../components/poolsense/translations/es.json | 4 +-- .../components/poolsense/translations/fr.json | 4 +-- .../components/poolsense/translations/it.json | 4 +-- .../components/poolsense/translations/ko.json | 4 +-- .../components/poolsense/translations/lb.json | 4 +-- .../components/poolsense/translations/no.json | 4 +-- .../components/poolsense/translations/pl.json | 4 +-- .../components/poolsense/translations/pt.json | 4 +-- .../components/poolsense/translations/ru.json | 4 +-- .../poolsense/translations/zh-Hans.json | 3 -- .../poolsense/translations/zh-Hant.json | 4 +-- .../components/rfxtrx/translations/it.json | 4 --- .../components/rfxtrx/translations/pt.json | 4 --- .../components/smappee/translations/ca.json | 3 +- .../components/smappee/translations/cs.json | 7 ----- .../components/smappee/translations/en.json | 3 +- .../components/smappee/translations/es.json | 3 +- .../components/smappee/translations/fr.json | 3 +- .../components/smappee/translations/it.json | 3 +- .../components/smappee/translations/ko.json | 3 +- .../components/smappee/translations/lb.json | 3 +- .../components/smappee/translations/no.json | 3 +- .../components/smappee/translations/pl.json | 3 +- .../components/smappee/translations/ru.json | 3 +- .../smappee/translations/zh-Hant.json | 3 +- .../components/spider/translations/ca.json | 19 ++++++++++++ .../components/spider/translations/es.json | 20 +++++++++++++ .../components/spider/translations/no.json | 20 +++++++++++++ .../components/spider/translations/ru.json | 20 +++++++++++++ .../tellduslive/translations/bg.json | 1 - .../tellduslive/translations/ca.json | 1 - .../tellduslive/translations/da.json | 1 - .../tellduslive/translations/de.json | 1 - .../tellduslive/translations/en.json | 1 - .../tellduslive/translations/es-419.json | 1 - .../tellduslive/translations/es.json | 1 - .../tellduslive/translations/fr.json | 1 - .../tellduslive/translations/hu.json | 1 - .../tellduslive/translations/it.json | 1 - .../tellduslive/translations/ko.json | 1 - .../tellduslive/translations/lb.json | 1 - .../tellduslive/translations/nl.json | 1 - .../tellduslive/translations/no.json | 1 - .../tellduslive/translations/pl.json | 1 - .../tellduslive/translations/pt-BR.json | 1 - .../tellduslive/translations/pt.json | 1 - .../tellduslive/translations/ru.json | 1 - .../tellduslive/translations/sl.json | 1 - .../tellduslive/translations/sv.json | 1 - .../tellduslive/translations/zh-Hans.json | 1 - .../tellduslive/translations/zh-Hant.json | 1 - .../components/toon/translations/bg.json | 28 +---------------- .../components/toon/translations/ca.json | 26 +--------------- .../components/toon/translations/cs.json | 6 ---- .../components/toon/translations/da.json | 28 +---------------- .../components/toon/translations/de.json | 28 +---------------- .../components/toon/translations/en.json | 26 +--------------- .../components/toon/translations/es-419.json | 28 +---------------- .../components/toon/translations/es.json | 26 +--------------- .../components/toon/translations/fi.json | 15 ---------- .../components/toon/translations/fr.json | 26 +--------------- .../components/toon/translations/hu.json | 12 -------- .../components/toon/translations/it.json | 26 +--------------- .../components/toon/translations/ko.json | 26 +--------------- .../components/toon/translations/lb.json | 26 +--------------- .../components/toon/translations/lt.json | 11 ------- .../components/toon/translations/nl.json | 28 +---------------- .../components/toon/translations/nn.json | 11 ------- .../components/toon/translations/no.json | 26 +--------------- .../components/toon/translations/pl.json | 26 +--------------- .../components/toon/translations/pt-BR.json | 28 +---------------- .../components/toon/translations/pt.json | 18 +---------- .../components/toon/translations/ru.json | 26 +--------------- .../components/toon/translations/sl.json | 28 +---------------- .../components/toon/translations/sv.json | 28 +---------------- .../components/toon/translations/th.json | 12 -------- .../components/toon/translations/zh-Hans.json | 12 -------- .../components/toon/translations/zh-Hant.json | 26 +--------------- .../transmission/translations/ca.json | 2 -- .../transmission/translations/en.json | 2 -- .../transmission/translations/es.json | 2 -- .../transmission/translations/fr.json | 2 -- .../transmission/translations/it.json | 2 -- .../transmission/translations/ko.json | 2 -- .../transmission/translations/lb.json | 2 -- .../transmission/translations/no.json | 2 -- .../transmission/translations/pl.json | 2 -- .../transmission/translations/pt.json | 2 -- .../transmission/translations/ru.json | 2 -- .../transmission/translations/zh-Hant.json | 2 -- 236 files changed, 300 insertions(+), 939 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/sensor.ca.json create mode 100644 homeassistant/components/hlk_sw16/translations/ca.json create mode 100644 homeassistant/components/ovo_energy/translations/ca.json create mode 100644 homeassistant/components/ovo_energy/translations/no.json create mode 100644 homeassistant/components/ovo_energy/translations/ru.json delete mode 100644 homeassistant/components/rfxtrx/translations/it.json delete mode 100644 homeassistant/components/rfxtrx/translations/pt.json delete mode 100644 homeassistant/components/smappee/translations/cs.json create mode 100644 homeassistant/components/spider/translations/ca.json create mode 100644 homeassistant/components/spider/translations/es.json create mode 100644 homeassistant/components/spider/translations/no.json create mode 100644 homeassistant/components/spider/translations/ru.json delete mode 100644 homeassistant/components/toon/translations/fi.json delete mode 100644 homeassistant/components/toon/translations/hu.json delete mode 100644 homeassistant/components/toon/translations/lt.json delete mode 100644 homeassistant/components/toon/translations/nn.json delete mode 100644 homeassistant/components/toon/translations/th.json delete mode 100644 homeassistant/components/toon/translations/zh-Hans.json diff --git a/homeassistant/components/accuweather/translations/sensor.ca.json b/homeassistant/components/accuweather/translations/sensor.ca.json new file mode 100644 index 00000000000..c2395047ccb --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Caient", + "rising": "Augmentant", + "steady": "Estable" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index bc8dff7fedd..46fdb6f0c96 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438\u0437\u0438\u0441\u043a\u0432\u0430 AdGuard Home {minimal_version} \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 {minimal_version}, \u0438\u043c\u0430\u0442\u0435 {current_version}. \u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u0437\u0430 Hass.io AdGuard Home.", - "adguard_home_outdated": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438\u0437\u0438\u0441\u043a\u0432\u0430 AdGuard Home {minimal_version} \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 {minimal_version}, \u0438\u043c\u0430\u0442\u0435 {current_version}.", "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home." }, diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json index ae142c7382c..b263e433cfb 100644 --- a/homeassistant/components/adguard/translations/ca.json +++ b/homeassistant/components/adguard/translations/ca.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}. Actualitza el complement de Hass.io d'AdGuard Home.", - "adguard_home_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}.", "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.", "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'AdGuard Home." }, diff --git a/homeassistant/components/adguard/translations/da.json b/homeassistant/components/adguard/translations/da.json index 20fd721eccc..edc33cd7927 100644 --- a/homeassistant/components/adguard/translations/da.json +++ b/homeassistant/components/adguard/translations/da.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}. Opdater venligst din Hass.io AdGuard Home-tilf\u00f8jelse.", - "adguard_home_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}.", "existing_instance_updated": "Opdaterede eksisterende konfiguration.", "single_instance_allowed": "Kun en enkelt konfiguration af AdGuard Home er tilladt." }, diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index 2e320d65e39..4de9d34f134 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Diese Integration erfordert AdGuard Home {minimal_version} oder h\u00f6her, du hast {current_version}. Bitte aktualisiere dein Hass.io AdGuard Home Add-on.", - "adguard_home_outdated": "Diese Integration erfordert AdGuard Home {minimal_version} oder h\u00f6her, du hast {current_version}.", "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig." }, diff --git a/homeassistant/components/adguard/translations/en.json b/homeassistant/components/adguard/translations/en.json index ffeb11af839..b52d0e5afd8 100644 --- a/homeassistant/components/adguard/translations/en.json +++ b/homeassistant/components/adguard/translations/en.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", - "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", "existing_instance_updated": "Updated existing configuration.", "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." }, diff --git a/homeassistant/components/adguard/translations/es-419.json b/homeassistant/components/adguard/translations/es-419.json index 450bb05c886..8e6ee0ab41d 100644 --- a/homeassistant/components/adguard/translations/es-419.json +++ b/homeassistant/components/adguard/translations/es-419.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}. Actualice su complemento Hass.io AdGuard Home.", - "adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}.", "existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.", "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index 70900e15eb5..82ba749dcc8 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}. Por favor, actualice su complemento Hass.io AdGuard Home.", - "adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}.", "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, diff --git a/homeassistant/components/adguard/translations/fr.json b/homeassistant/components/adguard/translations/fr.json index 3819aa9ec76..bf275f922f1 100644 --- a/homeassistant/components/adguard/translations/fr.json +++ b/homeassistant/components/adguard/translations/fr.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}. Veuillez mettre \u00e0 jour votre compl\u00e9ment Hass.io AdGuard Home.", - "adguard_home_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}.", "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.", "single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e." }, diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json index 0100f2914b4..ac2a903c5e5 100644 --- a/homeassistant/components/adguard/translations/it.json +++ b/homeassistant/components/adguard/translations/it.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo AdGuard Home di Hass.io.", - "adguard_home_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}.", "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home." }, diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json index cdb453b930f..9c0a7b8fbba 100644 --- a/homeassistant/components/adguard/translations/ko.json +++ b/homeassistant/components/adguard/translations/ko.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4. Hass.io AdGuard Home \uc560\ub4dc\uc628\uc744 \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uc138\uc694.", - "adguard_home_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4.", "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.", "single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/adguard/translations/lb.json b/homeassistant/components/adguard/translations/lb.json index 97cefb46bc0..abf6df83a6a 100644 --- a/homeassistant/components/adguard/translations/lb.json +++ b/homeassistant/components/adguard/translations/lb.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}. Aktualis\u00e9iert w.e.g. \u00e4ren Hass.io AdGuard Home Add-on.", - "adguard_home_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}.", "existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.", "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun AdGuard Home ass erlaabt." }, diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index 6d09824a699..61d009b503e 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}. Update uw Hass.io AdGuard Home-add-on.", - "adguard_home_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}.", "existing_instance_updated": "Bestaande configuratie bijgewerkt.", "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan." }, diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index bcd6fa5361d..a772988c042 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}. Vennligst oppdater Hass.io AdGuard Home-tillegget.", - "adguard_home_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}.", "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", "single_instance_allowed": "Kun en konfigurasjon av AdGuard Hjemer tillatt." }, diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index 6451f902642..ed034fcd1db 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}. Zaktualizuj sw\u00f3j dodatek Hass.io AdGuard Home.", - "adguard_home_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}.", "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119.", "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home." }, diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index e9998587353..ed65b1423bd 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0414\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f {minimal_version}, \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0430\u044f. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 Hass.io.", - "adguard_home_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u044e {minimal_version} \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e.", "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, diff --git a/homeassistant/components/adguard/translations/sl.json b/homeassistant/components/adguard/translations/sl.json index 45f0f2a2d0d..11c21a8bcc9 100644 --- a/homeassistant/components/adguard/translations/sl.json +++ b/homeassistant/components/adguard/translations/sl.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}. Prosimo posodobite va\u0161 hass.io AdGuard Home dodatek.", - "adguard_home_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}.", "existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.", "single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home." }, diff --git a/homeassistant/components/adguard/translations/sv.json b/homeassistant/components/adguard/translations/sv.json index ce501bf459c..98aa6c2a9c0 100644 --- a/homeassistant/components/adguard/translations/sv.json +++ b/homeassistant/components/adguard/translations/sv.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}. Uppdatera ditt Hass.io AdGuard Home-till\u00e4gg.", - "adguard_home_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}.", "existing_instance_updated": "Uppdaterade existerande konfiguration.", "single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten." }, diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json index 3bbe86352b8..5fbfdbbc59c 100644 --- a/homeassistant/components/adguard/translations/zh-Hant.json +++ b/homeassistant/components/adguard/translations/zh-Hant.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002\u8acb\u66f4\u65b0 Hass.io AdGuard Home \u5143\u4ef6\u3002", - "adguard_home_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002", "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 AdGuard Home\u3002" }, diff --git a/homeassistant/components/blink/translations/ca.json b/homeassistant/components/blink/translations/ca.json index 1984840b3ea..a79866fe614 100644 --- a/homeassistant/components/blink/translations/ca.json +++ b/homeassistant/components/blink/translations/ca.json @@ -4,6 +4,8 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_access_token": "Token d'acc\u00e9s no v\u00e0lid", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/blink/translations/en.json b/homeassistant/components/blink/translations/en.json index 415bc3dd1c9..9a0a2636d3e 100644 --- a/homeassistant/components/blink/translations/en.json +++ b/homeassistant/components/blink/translations/en.json @@ -4,6 +4,8 @@ "already_configured": "Device is already configured" }, "error": { + "cannot_connect": "Failed to connect", + "invalid_access_token": "Invalid access token", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, @@ -12,7 +14,7 @@ "data": { "2fa": "Two-factor code" }, - "description": "Enter the pin sent to your email. If the email does not contain a pin, leave blank", + "description": "Enter the pin sent to your email", "title": "Two-factor authentication" }, "user": { diff --git a/homeassistant/components/blink/translations/no.json b/homeassistant/components/blink/translations/no.json index fcfee086fb2..ca568389cd6 100644 --- a/homeassistant/components/blink/translations/no.json +++ b/homeassistant/components/blink/translations/no.json @@ -4,6 +4,8 @@ "already_configured": "Enheten er allerde konfigurert" }, "error": { + "cannot_connect": "Tilkobling mislyktes.", + "invalid_access_token": "Ugyldig tilgangstoken", "invalid_auth": "Ugyldig legitimasjon", "unknown": "Uventet feil" }, @@ -12,7 +14,7 @@ "data": { "2fa": "To-faktorskode" }, - "description": "Skriv inn pinnen som er sendt til din e-post. Hvis e-posten ikke inneholder en pin, m\u00e5 du la den st\u00e5 tom", + "description": "Skriv inn pin-koden som sendes til e-posten din", "title": "Totrinnsverifisering" }, "user": { diff --git a/homeassistant/components/blink/translations/ru.json b/homeassistant/components/blink/translations/ru.json index ea95319f856..72dcb497cec 100644 --- a/homeassistant/components/blink/translations/ru.json +++ b/homeassistant/components/blink/translations/ru.json @@ -4,6 +4,8 @@ "already_configured": "\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." }, "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, @@ -12,7 +14,7 @@ "data": { "2fa": "\u041a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 \u0412\u0430\u0448\u0443 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443\u044e \u043f\u043e\u0447\u0442\u0443. \u0415\u0441\u043b\u0438 \u043f\u0438\u0441\u044c\u043c\u043e \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 PIN-\u043a\u043e\u0434, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 \u0412\u0430\u0448\u0443 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443\u044e \u043f\u043e\u0447\u0442\u0443", "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { diff --git a/homeassistant/components/bond/translations/ca.json b/homeassistant/components/bond/translations/ca.json index b068144df09..8cccca1afc8 100644 --- a/homeassistant/components/bond/translations/ca.json +++ b/homeassistant/components/bond/translations/ca.json @@ -8,7 +8,14 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, + "flow_title": "Bond: {bond_id} ({host})", "step": { + "confirm": { + "data": { + "access_token": "Token d'acc\u00e9s" + }, + "description": "Voleu configurar {bond_id}?" + }, "user": { "data": { "access_token": "Token d'acc\u00e9s", diff --git a/homeassistant/components/harmony/translations/ca.json b/homeassistant/components/harmony/translations/ca.json index 0406160a7a6..5bb279c0482 100644 --- a/homeassistant/components/harmony/translations/ca.json +++ b/homeassistant/components/harmony/translations/ca.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "Activitat predeterminada a executar quan no se n'especifica cap.", - "activity_notify": "Actualitza l'activitat actual al canviar l'activitat.", "delay_secs": "Retard entre l'enviament d'ordres." }, "description": "Ajusta les opcions de Harmony Hub" diff --git a/homeassistant/components/harmony/translations/en.json b/homeassistant/components/harmony/translations/en.json index d180ff4ba7d..ce13e79e279 100644 --- a/homeassistant/components/harmony/translations/en.json +++ b/homeassistant/components/harmony/translations/en.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "The default activity to execute when none is specified.", - "activity_notify": "Update current activity on start of activity switch.", "delay_secs": "The delay between sending commands." }, "description": "Adjust Harmony Hub Options" diff --git a/homeassistant/components/harmony/translations/es.json b/homeassistant/components/harmony/translations/es.json index 9fbca3b97d3..39305d30680 100644 --- a/homeassistant/components/harmony/translations/es.json +++ b/homeassistant/components/harmony/translations/es.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "La actividad por defecto a ejecutar cuando no se especifica ninguna.", - "activity_notify": "Actualice la actividad actual al inicio del cambio de actividad.", "delay_secs": "El retraso entre el env\u00edo de comandos." }, "description": "Ajustar las opciones de Harmony Hub" diff --git a/homeassistant/components/harmony/translations/fr.json b/homeassistant/components/harmony/translations/fr.json index 78f85d98552..4343ec3139d 100644 --- a/homeassistant/components/harmony/translations/fr.json +++ b/homeassistant/components/harmony/translations/fr.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "Activit\u00e9 par d\u00e9faut \u00e0 ex\u00e9cuter lorsqu'aucune n'est sp\u00e9cifi\u00e9e.", - "activity_notify": "Mettre \u00e0 jour l'activit\u00e9 lors de son lancement.", "delay_secs": "Le d\u00e9lai entre l'envoi des commandes." }, "description": "Ajuster les options du hub Harmony" diff --git a/homeassistant/components/harmony/translations/it.json b/homeassistant/components/harmony/translations/it.json index bd3bece0a4f..c658e69e0c0 100644 --- a/homeassistant/components/harmony/translations/it.json +++ b/homeassistant/components/harmony/translations/it.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "L'attivit\u00e0 predefinita da eseguire quando nessuna \u00e8 specificata.", - "activity_notify": "Aggiorna l'attivit\u00e0 corrente all'avvio del cambio attivit\u00e0.", "delay_secs": "Il ritardo tra l'invio dei comandi." }, "description": "Regolare le opzioni di Harmony Hub" diff --git a/homeassistant/components/harmony/translations/ko.json b/homeassistant/components/harmony/translations/ko.json index 6e439f9c744..528f5e9cc7e 100644 --- a/homeassistant/components/harmony/translations/ko.json +++ b/homeassistant/components/harmony/translations/ko.json @@ -27,7 +27,6 @@ "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/harmony/translations/no.json b/homeassistant/components/harmony/translations/no.json index 14df560d104..c3518792851 100644 --- a/homeassistant/components/harmony/translations/no.json +++ b/homeassistant/components/harmony/translations/no.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "Standardaktiviteten som skal utf\u00f8res n\u00e5r ingen er angitt.", - "activity_notify": "Oppdater gjeldende aktivitet ved starten av aktivitetsbryteren.", "delay_secs": "Forsinkelsen mellom sending av kommandoer." }, "description": "Juster alternativene for harmonihub" diff --git a/homeassistant/components/harmony/translations/pl.json b/homeassistant/components/harmony/translations/pl.json index d8a3c22f3c3..12bbcfaca18 100644 --- a/homeassistant/components/harmony/translations/pl.json +++ b/homeassistant/components/harmony/translations/pl.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "Domy\u015blna aktywno\u015b\u0107 do wykonania, gdy \u017cadnej nie okre\u015blono.", - "activity_notify": "Aktualizowanie bie\u017c\u0105cej aktywno\u015bci przy rozpoczynaniu prze\u0142\u0105czania aktywno\u015bci.", "delay_secs": "Op\u00f3\u017anienie mi\u0119dzy wysy\u0142aniem polece\u0144." }, "description": "Dostosuj opcje huba Harmony" diff --git a/homeassistant/components/harmony/translations/ru.json b/homeassistant/components/harmony/translations/ru.json index b2bed0c71f2..4e995a26c48 100644 --- a/homeassistant/components/harmony/translations/ru.json +++ b/homeassistant/components/harmony/translations/ru.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "\u0410\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e, \u043a\u043e\u0433\u0434\u0430 \u043d\u0438 \u043e\u0434\u043d\u0430 \u0438\u0437 \u043d\u0438\u0445 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u0430.", - "activity_notify": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0443\u044e \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438.", "delay_secs": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 \u043c\u0435\u0436\u0434\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u043e\u0439 \u043a\u043e\u043c\u0430\u043d\u0434." }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 Harmony Hub" diff --git a/homeassistant/components/harmony/translations/zh-Hant.json b/homeassistant/components/harmony/translations/zh-Hant.json index d7ae177efee..dfd1249d629 100644 --- a/homeassistant/components/harmony/translations/zh-Hant.json +++ b/homeassistant/components/harmony/translations/zh-Hant.json @@ -27,7 +27,6 @@ "init": { "data": { "activity": "\u7576\u672a\u6307\u5b9a\u6642\u9810\u8a2d\u57f7\u884c\u6d3b\u52d5\u3002", - "activity_notify": "\u65bc\u958b\u59cb\u6d3b\u52d5\u958b\u95dc\u6642\u66f4\u65b0\u76ee\u524d\u6d3b\u52d5\u3002", "delay_secs": "\u50b3\u9001\u547d\u4ee4\u9593\u9694\u79d2\u6578\u3002" }, "description": "\u8abf\u6574 Harmony Hub \u9078\u9805" diff --git a/homeassistant/components/hlk_sw16/translations/ca.json b/homeassistant/components/hlk_sw16/translations/ca.json new file mode 100644 index 00000000000..df8218bab3e --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index 0282b701ad7..6389de72e4e 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "[%key::component::homekit::config::step::user::data::auto_start%]", - "safe_mode": "Mode segur (habilita-ho nom\u00e9s si falla la vinculaci\u00f3)", - "zeroconf_default_interface": "Utilitza la interf\u00edcie zeroconf predeterminada. Activa-ho si no es pot trobar l'enlla\u00e7 a l'aplicaci\u00f3 Casa (Home app)." + "safe_mode": "Mode segur (habilita-ho nom\u00e9s si falla la vinculaci\u00f3)" }, "description": "Aquests par\u00e0metres nom\u00e9s s'han d'ajustar si l'enlla\u00e7 HomeKit no \u00e9s funcional.", "title": "Configuraci\u00f3 avan\u00e7ada" diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 1fc5ae59a0c..a70f9e90ed7 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", - "safe_mode": "Safe Mode (enable only if pairing fails)", - "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + "safe_mode": "Safe Mode (enable only if pairing fails)" }, "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", "title": "Advanced Configuration" diff --git a/homeassistant/components/homekit/translations/es-419.json b/homeassistant/components/homekit/translations/es-419.json index 06c66697fbb..9288849b8ee 100644 --- a/homeassistant/components/homekit/translations/es-419.json +++ b/homeassistant/components/homekit/translations/es-419.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "Inicio autom\u00e1tico (deshabilitar si se usa Z-Wave u otro sistema de inicio diferido)", - "safe_mode": "Modo seguro (habil\u00edtelo solo si falla el emparejamiento)", - "zeroconf_default_interface": "Use la interfaz zeroconf predeterminada (habil\u00edtela si no se puede encontrar el puente en la aplicaci\u00f3n Inicio)" + "safe_mode": "Modo seguro (habil\u00edtelo solo si falla el emparejamiento)" }, "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", "title": "Configuraci\u00f3n avanzada" diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 92f64c435de..b7172574b12 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)", - "safe_mode": "Modo seguro (habil\u00edtelo solo si falla el emparejamiento)", - "zeroconf_default_interface": "Use la interfaz zeroconf predeterminada (habil\u00edtela si no se puede encontrar el puente en la aplicaci\u00f3n Inicio)" + "safe_mode": "Modo seguro (habil\u00edtelo solo si falla el emparejamiento)" }, "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", "title": "Configuraci\u00f3n avanzada" diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index 17cc5149654..3c6162e378e 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", - "safe_mode": "Mode sans \u00e9chec (activez uniquement si le jumelage \u00e9choue)", - "zeroconf_default_interface": "Utiliser l'interface zeroconf par d\u00e9faut (activer si le pont est introuvable dans l'application Home)" + "safe_mode": "Mode sans \u00e9chec (activez uniquement si le jumelage \u00e9choue)" }, "description": "Ces param\u00e8tres ne doivent \u00eatre ajust\u00e9s que si le pont HomeKit n'est pas fonctionnel.", "title": "Configuration avanc\u00e9e" diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 6ebd08c88f3..9c139c46290 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", - "safe_mode": "Modalit\u00e0 provvisoria (attivare solo in caso di errore di associazione)", - "zeroconf_default_interface": "Utilizzare l'interfaccia zeroconf predefinita (abilitare se il bridge non pu\u00f2 essere trovato nell'app Home)" + "safe_mode": "Modalit\u00e0 provvisoria (attivare solo in caso di errore di associazione)" }, "description": "Queste impostazioni devono essere modificate solo se il bridge HomeKit non funziona.", "title": "Configurazione Avanzata" diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json index 7f9d6d26d11..5922fac7af6 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", - "safe_mode": "\uc548\uc804 \ubaa8\ub4dc (\ud398\uc5b4\ub9c1\uc774 \uc2e4\ud328\ud55c \uacbd\uc6b0\uc5d0\ub9cc \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", - "zeroconf_default_interface": "\uae30\ubcf8 zeroconf \uc778\ud130\ud398\uc774\uc2a4 \uc0ac\uc6a9 (Home \uc571\uc5d0\uc11c \ube0c\ub9ac\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\ub294 \uacbd\uc6b0 \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" + "safe_mode": "\uc548\uc804 \ubaa8\ub4dc (\ud398\uc5b4\ub9c1\uc774 \uc2e4\ud328\ud55c \uacbd\uc6b0\uc5d0\ub9cc \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" }, "description": "\uc774 \uc124\uc815\uc740 HomeKit \ube0c\ub9ac\uc9c0\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "\uace0\uae09 \uad6c\uc131\ud558\uae30" diff --git a/homeassistant/components/homekit/translations/lb.json b/homeassistant/components/homekit/translations/lb.json index 14c385f213b..dd2583a2ad3 100644 --- a/homeassistant/components/homekit/translations/lb.json +++ b/homeassistant/components/homekit/translations/lb.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "Autostart (d\u00e9aktiv\u00e9ier falls Z-Wave oder een aanere verz\u00f6gerte Start System benotzt g\u00ebtt)", - "safe_mode": "Safe Mode (n\u00ebmmen aktiv\u00e9ieren wann Kopplung net geht)", - "zeroconf_default_interface": "Standard Zeroconf Interface benotzen (aktiv\u00e9ieren falls d'Bridge net an der Home App fonnt g\u00ebtt)" + "safe_mode": "Safe Mode (n\u00ebmmen aktiv\u00e9ieren wann Kopplung net geht)" }, "description": "D\u00ebs Astellungen brauche n\u00ebmmen ajust\u00e9iert ze ginn fals d'HomeKit Bridge net funktion\u00e9iert.", "title": "Erweidert Konfiguratioun" diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 38599b5476f..1e59d947e60 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "Autostart (deaktiver hvis du bruker Z-Wave eller annet forsinket startsystem)", - "safe_mode": "Sikker modus (aktiver bare hvis sammenkoblingen mislykkes)", - "zeroconf_default_interface": "Bruk standard zeroconf-grensesnitt (aktiver hvis broen ikke kan finnes i Hjem-appen)" + "safe_mode": "Sikker modus (aktiver bare hvis sammenkoblingen mislykkes)" }, "description": "Disse innstillingene m\u00e5 bare justeres dersom HomeKit bridge er ikke i bruk.", "title": "Avansert konfigurasjon" diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index dca8c10b8de..240031ef5f1 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", - "safe_mode": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0431\u043e\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f)", - "zeroconf_default_interface": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c zeroconf \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0431\u0440\u0438\u0434\u0436 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 '\u0414\u043e\u043c')." + "safe_mode": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0431\u043e\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f)" }, "description": "\u042d\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 HomeKit Bridge \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", "title": "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" diff --git a/homeassistant/components/homekit/translations/sl.json b/homeassistant/components/homekit/translations/sl.json index bfc315824ee..0416e80b98d 100644 --- a/homeassistant/components/homekit/translations/sl.json +++ b/homeassistant/components/homekit/translations/sl.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "Samodejni zagon (onemogo\u010dite, \u010de uporabljate Z-wave ali kakteri drug sistem z zakasnjenim zagonom)", - "safe_mode": "Varni na\u010din (omogo\u010dite samo, \u010de seznanjanje ne uspe)", - "zeroconf_default_interface": "Uporabite privzeti zeroconf vmesnik (omogo\u010dite, \u010de mostu ni mogo\u010de najti v Home Assistant-u)" + "safe_mode": "Varni na\u010din (omogo\u010dite samo, \u010de seznanjanje ne uspe)" }, "description": "Te nastavitve je treba prilagoditi le, \u010de most HomeKit ni funkcionalen.", "title": "Napredna konfiguracija" diff --git a/homeassistant/components/homekit/translations/zh-Hans.json b/homeassistant/components/homekit/translations/zh-Hans.json index 24d263657af..0c2d49149ef 100644 --- a/homeassistant/components/homekit/translations/zh-Hans.json +++ b/homeassistant/components/homekit/translations/zh-Hans.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u8fdf\u542f\u52a8\u7cfb\u7edf\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", - "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u4ec5\u5728\u914d\u5bf9\u5931\u8d25\u65f6\u542f\u7528\uff09", - "zeroconf_default_interface": "\u4f7f\u7528\u9ed8\u8ba4\u7684 zeroconf \u63a5\u53e3\uff08\u5982\u679c\u5728\u201c\u5bb6\u5ead\u201d\u5e94\u7528\u7a0b\u5e8f\u4e2d\u627e\u4e0d\u5230\u6865\u63a5\u5668\u5219\u542f\u7528\uff09" + "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u4ec5\u5728\u914d\u5bf9\u5931\u8d25\u65f6\u542f\u7528\uff09" }, "description": "\u8fd9\u4e9b\u8bbe\u7f6e\u53ea\u6709\u5f53 HomeKit \u6865\u63a5\u5668\u529f\u80fd\u4e0d\u6b63\u5e38\u65f6\u624d\u9700\u8981\u8c03\u6574\u3002", "title": "\u9ad8\u7ea7\u914d\u7f6e" diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 166fefb9843..6fbdfc3aad1 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -23,8 +23,7 @@ "advanced": { "data": { "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", - "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u50c5\u65bc\u914d\u5c0d\u5931\u6557\u6642\u4f7f\u7528\uff09", - "zeroconf_default_interface": "\u4f7f\u7528\u9810\u8a2d zeroconf \u4ecb\u9762\uff08\u50c5\u65bc\u5bb6\u5ead App \u627e\u4e0d\u5230 Bridge \u6642\u958b\u555f\uff09" + "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u50c5\u65bc\u914d\u5c0d\u5931\u6557\u6642\u4f7f\u7528\uff09" }, "description": "\u50c5\u65bc Homekit bridge \u7121\u6cd5\u6b63\u5e38\u4f7f\u7528\u6642\uff0c\u8abf\u6574\u6b64\u4e9b\u8a2d\u5b9a\u3002", "title": "\u9032\u968e\u8a2d\u5b9a" diff --git a/homeassistant/components/hue/translations/ca.json b/homeassistant/components/hue/translations/ca.json index 26fab369ad8..97098b21db7 100644 --- a/homeassistant/components/hue/translations/ca.json +++ b/homeassistant/components/hue/translations/ca.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Permet grups Hue", "allow_hue_groups": "Permet grups Hue", "allow_unreachable": "Permet que bombetes no accessibles puguin informar del seu estat correctament" } diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index cce17f19e38..c9c8c96f4d5 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -55,7 +55,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Erlaube Hue Gruppen", "allow_hue_groups": "Erlaube Hue Gruppen", "allow_unreachable": "Erlauben Sie unerreichbaren Gl\u00fchbirnen, ihren Zustand korrekt zu melden" } diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index 82f6c1e74be..9f3a96f2da3 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Allow Hue groups", "allow_hue_groups": "Allow Hue groups", "allow_unreachable": "Allow unreachable bulbs to report their state correctly" } diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index aa1fe26c775..767c883a87b 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Permitir grupos de Hue", "allow_hue_groups": "Permitir grupos de Hue", "allow_unreachable": "Permitir que las bombillas inalcanzables informen su estado correctamente" } diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index 280afac5df6..99e82f1a89b 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -56,7 +56,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Autoriser les groupes Hue", "allow_hue_groups": "Autoriser les groupes Hue", "allow_unreachable": "Autoriser les ampoules inaccessibles \u00e0 signaler correctement leur \u00e9tat" } diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json index b2320be646b..6fc4554b384 100644 --- a/homeassistant/components/hue/translations/it.json +++ b/homeassistant/components/hue/translations/it.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Consenti gruppi Hue", "allow_hue_groups": "Consenti gruppi Hue", "allow_unreachable": "Consentire alle lampadine irraggiungibili di segnalare correttamente il loro stato" } diff --git a/homeassistant/components/hue/translations/ko.json b/homeassistant/components/hue/translations/ko.json index 8387895dab3..050b5c51c97 100644 --- a/homeassistant/components/hue/translations/ko.json +++ b/homeassistant/components/hue/translations/ko.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Hue \uadf8\ub8f9 \ud5c8\uc6a9", "allow_hue_groups": "Hue \uadf8\ub8f9 \ud5c8\uc6a9", "allow_unreachable": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\ub294 \uc804\uad6c\uac00 \uc0c1\ud0dc\ub97c \uc62c\ubc14\ub974\uac8c \ubcf4\uace0\ud558\ub3c4\ub85d \ud5c8\uc6a9" } diff --git a/homeassistant/components/hue/translations/lb.json b/homeassistant/components/hue/translations/lb.json index b6af356f387..46960929404 100644 --- a/homeassistant/components/hue/translations/lb.json +++ b/homeassistant/components/hue/translations/lb.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Hue Gruppen erlaaben", "allow_hue_groups": "Hue Gruppen erlaaben" } } diff --git a/homeassistant/components/hue/translations/no.json b/homeassistant/components/hue/translations/no.json index c5e9cedd708..2d51ee26452 100644 --- a/homeassistant/components/hue/translations/no.json +++ b/homeassistant/components/hue/translations/no.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Tillat Hue-grupper", "allow_hue_groups": "Tillat Hue-grupper", "allow_unreachable": "Tillat uoppn\u00e5elige p\u00e6rer \u00e5 rapportere sin tilstand riktig" } diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index 01147d35663..cbfd5ecfb5d 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "Zezwalaj na grupy Hue", "allow_unreachable": "Zezwalaj nieosi\u0105galnym \u017car\u00f3wkom na poprawne raportowanie ich stanu" } } diff --git a/homeassistant/components/hue/translations/ru.json b/homeassistant/components/hue/translations/ru.json index f2a88637a62..f302eeaa473 100644 --- a/homeassistant/components/hue/translations/ru.json +++ b/homeassistant/components/hue/translations/ru.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b Hue", "allow_hue_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b Hue", "allow_unreachable": "\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0441\u043e\u043e\u0431\u0449\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" } diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index 47b8e13de34..2442ae30d10 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -58,7 +58,6 @@ "step": { "init": { "data": { - "allow_how_groups": "\u5141\u8a31 Hue \u7fa4\u7d44", "allow_hue_groups": "\u5141\u8a31 Hue \u7fa4\u7d44", "allow_unreachable": "\u5141\u8a31\u7121\u6cd5\u9023\u7dda\u7684\u71c8\u6ce1\u6b63\u78ba\u56de\u5831\u5176\u72c0\u614b" } diff --git a/homeassistant/components/iqvia/translations/bg.json b/homeassistant/components/iqvia/translations/bg.json index 8a233f30fff..26125dcaa14 100644 --- a/homeassistant/components/iqvia/translations/bg.json +++ b/homeassistant/components/iqvia/translations/bg.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "\u041f\u043e\u0449\u0435\u043d\u0441\u043a\u0438\u044f\u0442 \u043a\u043e\u0434 \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d", "invalid_zip_code": "\u041f\u043e\u0449\u0435\u043d\u0441\u043a\u0438\u044f\u0442 \u043a\u043e\u0434 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d" }, "step": { diff --git a/homeassistant/components/iqvia/translations/ca.json b/homeassistant/components/iqvia/translations/ca.json index cf75f845ef1..bfc1cf6b452 100644 --- a/homeassistant/components/iqvia/translations/ca.json +++ b/homeassistant/components/iqvia/translations/ca.json @@ -1,7 +1,9 @@ { "config": { + "abort": { + "already_configured": "Aquest codi postal ja s\u2019ha configurat." + }, "error": { - "identifier_exists": "Codi postal ja registrat", "invalid_zip_code": "Codi postal incorrecte" }, "step": { diff --git a/homeassistant/components/iqvia/translations/da.json b/homeassistant/components/iqvia/translations/da.json index ee478a67dd5..8cc93be50ec 100644 --- a/homeassistant/components/iqvia/translations/da.json +++ b/homeassistant/components/iqvia/translations/da.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Postnummer er allerede registreret", "invalid_zip_code": "Postnummer er ugyldig" }, "step": { diff --git a/homeassistant/components/iqvia/translations/de.json b/homeassistant/components/iqvia/translations/de.json index c42f9092d9e..6f076e8b998 100644 --- a/homeassistant/components/iqvia/translations/de.json +++ b/homeassistant/components/iqvia/translations/de.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Postleitzahl bereits registriert", "invalid_zip_code": "Postleitzahl ist ung\u00fcltig" }, "step": { diff --git a/homeassistant/components/iqvia/translations/en.json b/homeassistant/components/iqvia/translations/en.json index 6c96b78c854..c9b3526102b 100644 --- a/homeassistant/components/iqvia/translations/en.json +++ b/homeassistant/components/iqvia/translations/en.json @@ -4,7 +4,6 @@ "already_configured": "This ZIP code has already been configured." }, "error": { - "identifier_exists": "ZIP code already registered", "invalid_zip_code": "ZIP code is invalid" }, "step": { diff --git a/homeassistant/components/iqvia/translations/es-419.json b/homeassistant/components/iqvia/translations/es-419.json index 21b5273bfba..61fa3f885a2 100644 --- a/homeassistant/components/iqvia/translations/es-419.json +++ b/homeassistant/components/iqvia/translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "C\u00f3digo postal ya registrado", "invalid_zip_code": "El c\u00f3digo postal no es v\u00e1lido" }, "step": { diff --git a/homeassistant/components/iqvia/translations/es.json b/homeassistant/components/iqvia/translations/es.json index 9288503ed60..1d02f6e60c8 100644 --- a/homeassistant/components/iqvia/translations/es.json +++ b/homeassistant/components/iqvia/translations/es.json @@ -4,7 +4,6 @@ "already_configured": "Este c\u00f3digo postal ya ha sido configurado." }, "error": { - "identifier_exists": "C\u00f3digo postal ya registrado", "invalid_zip_code": "El c\u00f3digo postal no es v\u00e1lido" }, "step": { diff --git a/homeassistant/components/iqvia/translations/fi.json b/homeassistant/components/iqvia/translations/fi.json index 151e9755a6a..375176873f0 100644 --- a/homeassistant/components/iqvia/translations/fi.json +++ b/homeassistant/components/iqvia/translations/fi.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Postinumero on jo rekister\u00f6ity", "invalid_zip_code": "Postinumero on virheellinen" }, "step": { diff --git a/homeassistant/components/iqvia/translations/fr.json b/homeassistant/components/iqvia/translations/fr.json index c5b8d0cbd81..c10a1da59f2 100644 --- a/homeassistant/components/iqvia/translations/fr.json +++ b/homeassistant/components/iqvia/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Code postal d\u00e9j\u00e0 enregistr\u00e9", "invalid_zip_code": "Code postal invalide" }, "step": { diff --git a/homeassistant/components/iqvia/translations/it.json b/homeassistant/components/iqvia/translations/it.json index 85de80b5c97..599974a8d26 100644 --- a/homeassistant/components/iqvia/translations/it.json +++ b/homeassistant/components/iqvia/translations/it.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Il CAP \u00e8 gi\u00e0 registrato", "invalid_zip_code": "Il CAP non \u00e8 valido" }, "step": { diff --git a/homeassistant/components/iqvia/translations/ko.json b/homeassistant/components/iqvia/translations/ko.json index 6b7a63d102b..f3dd4f82b62 100644 --- a/homeassistant/components/iqvia/translations/ko.json +++ b/homeassistant/components/iqvia/translations/ko.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "\uc6b0\ud3b8\ubc88\ud638\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_zip_code": "\uc6b0\ud3b8\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { diff --git a/homeassistant/components/iqvia/translations/lb.json b/homeassistant/components/iqvia/translations/lb.json index 5a1f257f8f9..4941358ae08 100644 --- a/homeassistant/components/iqvia/translations/lb.json +++ b/homeassistant/components/iqvia/translations/lb.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Postleitzuel ass scho registr\u00e9iert", "invalid_zip_code": "Postleitzuel ass ong\u00eblteg" }, "step": { diff --git a/homeassistant/components/iqvia/translations/nl.json b/homeassistant/components/iqvia/translations/nl.json index a8e1ff5fa7d..9ecd9f12f11 100644 --- a/homeassistant/components/iqvia/translations/nl.json +++ b/homeassistant/components/iqvia/translations/nl.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Postcode al geregistreerd", "invalid_zip_code": "Postcode is ongeldig" }, "step": { diff --git a/homeassistant/components/iqvia/translations/no.json b/homeassistant/components/iqvia/translations/no.json index 9f130f52403..98526dfed24 100644 --- a/homeassistant/components/iqvia/translations/no.json +++ b/homeassistant/components/iqvia/translations/no.json @@ -4,7 +4,6 @@ "already_configured": "Denne postnummeren er allerede konfigurert." }, "error": { - "identifier_exists": "Postnummer er allerede registrert", "invalid_zip_code": "Postnummeret er ugyldig" }, "step": { diff --git a/homeassistant/components/iqvia/translations/pl.json b/homeassistant/components/iqvia/translations/pl.json index 38e10460ae9..f33a5257a60 100644 --- a/homeassistant/components/iqvia/translations/pl.json +++ b/homeassistant/components/iqvia/translations/pl.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Kod pocztowy jest ju\u017c zarejestrowany.", "invalid_zip_code": "Kod pocztowy jest nieprawid\u0142owy" }, "step": { diff --git a/homeassistant/components/iqvia/translations/pt-BR.json b/homeassistant/components/iqvia/translations/pt-BR.json index 3247c162e7b..d8ceb8fe934 100644 --- a/homeassistant/components/iqvia/translations/pt-BR.json +++ b/homeassistant/components/iqvia/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "C\u00f3digo postal j\u00e1 registado", "invalid_zip_code": "C\u00f3digo postal inv\u00e1lido" }, "step": { diff --git a/homeassistant/components/iqvia/translations/ru.json b/homeassistant/components/iqvia/translations/ru.json index d7b868acad3..dc320a69eb6 100644 --- a/homeassistant/components/iqvia/translations/ru.json +++ b/homeassistant/components/iqvia/translations/ru.json @@ -4,7 +4,6 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u044d\u0442\u0438\u043c \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u043c \u0438\u043d\u0434\u0435\u043a\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441." }, "step": { diff --git a/homeassistant/components/iqvia/translations/sl.json b/homeassistant/components/iqvia/translations/sl.json index 69297d6aef9..351ad666c37 100644 --- a/homeassistant/components/iqvia/translations/sl.json +++ b/homeassistant/components/iqvia/translations/sl.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Po\u0161tna \u0161tevilka je \u017ee registrirana", "invalid_zip_code": "Po\u0161tna \u0161tevilka ni veljavna" }, "step": { diff --git a/homeassistant/components/iqvia/translations/sv.json b/homeassistant/components/iqvia/translations/sv.json index 3e47ca925db..88054725823 100644 --- a/homeassistant/components/iqvia/translations/sv.json +++ b/homeassistant/components/iqvia/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Postnummer redan registrerat", "invalid_zip_code": "Ogiltigt postnummer" }, "step": { diff --git a/homeassistant/components/iqvia/translations/zh-Hans.json b/homeassistant/components/iqvia/translations/zh-Hans.json index 066fae277b1..9b5f24c1d5f 100644 --- a/homeassistant/components/iqvia/translations/zh-Hans.json +++ b/homeassistant/components/iqvia/translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "\u90ae\u653f\u7f16\u7801\u5df2\u88ab\u6ce8\u518c", "invalid_zip_code": "\u90ae\u653f\u7f16\u7801\u65e0\u6548" }, "step": { diff --git a/homeassistant/components/iqvia/translations/zh-Hant.json b/homeassistant/components/iqvia/translations/zh-Hant.json index 035b952d20b..68b19b69d21 100644 --- a/homeassistant/components/iqvia/translations/zh-Hant.json +++ b/homeassistant/components/iqvia/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "\u90f5\u905e\u5340\u865f\u5df2\u8a3b\u518a", "invalid_zip_code": "\u90f5\u905e\u5340\u865f\u7121\u6548" }, "step": { diff --git a/homeassistant/components/meteo_france/translations/ca.json b/homeassistant/components/meteo_france/translations/ca.json index 02f43e0d4a8..4147a4169b1 100644 --- a/homeassistant/components/meteo_france/translations/ca.json +++ b/homeassistant/components/meteo_france/translations/ca.json @@ -4,7 +4,17 @@ "already_configured": "Ciutat ja configurada", "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard" }, + "error": { + "empty": "No hi ha cap resultat de la cerca: comproveu el camp de la ciutat" + }, "step": { + "cities": { + "data": { + "city": "Ciutat" + }, + "description": "Tria la teva ciutat de la llista", + "title": "M\u00e9t\u00e9o-France" + }, "user": { "data": { "city": "Ciutat" @@ -13,5 +23,14 @@ "title": "M\u00e9t\u00e9o-France" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Mode de predicci\u00f3" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/ca.json b/homeassistant/components/minecraft_server/translations/ca.json index 6e5554216d9..ce395b75713 100644 --- a/homeassistant/components/minecraft_server/translations/ca.json +++ b/homeassistant/components/minecraft_server/translations/ca.json @@ -14,7 +14,7 @@ "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "Configuraci\u00f3 d'una inst\u00e0ncia de servidor de Minecraft per poder monitoritzar-lo.", + "description": "Configura una inst\u00e0ncia del teu servidor de Minecraft per poder monitoritzar-lo.", "title": "Enlla\u00e7 del servidor de Minecraft" } } diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 715327d004a..e94be2d1a6d 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -66,11 +66,13 @@ }, "options": { "data": { + "birth_enable": "Activa el missatge de naixement", "birth_payload": "Dades (payload) missatge de naixement", "birth_qos": "QoS missatge de naixement", "birth_retain": "Retenci\u00f3 missatge de naixement", "birth_topic": "Topic missatge de naixement", "discovery": "Activar descobriment", + "will_enable": "Activa el missatge de naixement", "will_payload": "Dades (payload) missatge d'\u00faltima voluntat", "will_qos": "QoS missatge d'\u00faltima voluntat", "will_retain": "Retenci\u00f3 missatge d'\u00faltima voluntat", diff --git a/homeassistant/components/netatmo/translations/ca.json b/homeassistant/components/netatmo/translations/ca.json index 9053c66ba72..99104c168cf 100644 --- a/homeassistant/components/netatmo/translations/ca.json +++ b/homeassistant/components/netatmo/translations/ca.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_setup": "[%key::common::config_flow::abort::single_instance_allowed%]", "authorize_url_timeout": "[%key::common::config_flow::abort::oauth2_authorize_url_timeout%]", - "missing_configuration": "[%key::common::config_flow::abort::oauth2_missing_configuration%]" + "missing_configuration": "[%key::common::config_flow::abort::oauth2_missing_configuration%]", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "create_entry": { "default": "[%key::common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/netatmo/translations/cs.json b/homeassistant/components/netatmo/translations/cs.json index 5c5b19b0bf8..5aedb333d6c 100644 --- a/homeassistant/components/netatmo/translations/cs.json +++ b/homeassistant/components/netatmo/translations/cs.json @@ -1,7 +1,6 @@ { "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." }, diff --git a/homeassistant/components/netatmo/translations/da.json b/homeassistant/components/netatmo/translations/da.json index fb82b9fb642..b762d06cd0d 100644 --- a/homeassistant/components/netatmo/translations/da.json +++ b/homeassistant/components/netatmo/translations/da.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_setup": "Du kan kun konfigurere \u00e9n Netatmo-konto." - }, "create_entry": { "default": "Korrekt godkendt med Netatmo." } diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index 79235828eca..30cfba6dfed 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "Bereits konfiguriert. Es ist nur eine einzige Konfiguration m\u00f6glich.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." }, diff --git a/homeassistant/components/netatmo/translations/en.json b/homeassistant/components/netatmo/translations/en.json index a96dfcbec6c..04ac8e69f11 100644 --- a/homeassistant/components/netatmo/translations/en.json +++ b/homeassistant/components/netatmo/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "Already configured. Only a single configuration possible.", "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation.", "single_instance_allowed": "Already configured. Only a single configuration possible." diff --git a/homeassistant/components/netatmo/translations/es-419.json b/homeassistant/components/netatmo/translations/es-419.json index 1de2505dd2d..0a6f07684f9 100644 --- a/homeassistant/components/netatmo/translations/es-419.json +++ b/homeassistant/components/netatmo/translations/es-419.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_setup": "Solo puede configurar una cuenta de Netatmo." - }, "create_entry": { "default": "Autenticado con \u00e9xito con Netatmo." } diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index a570537ab67..a72728e8438 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "authorize_url_timeout": "Tiempo excedido generando la url de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Por favor, consulta la documentaci\u00f3n." + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, consulta la documentaci\u00f3n.", + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index 099be273007..7a269a23d70 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "Vous ne pouvez configurer qu'un seul compte Netatmo.", "missing_configuration": "Ce composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "create_entry": { diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index 4104aba464c..cae1f6d20c0 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." }, diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index 1efe744687d..e01875998cc 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." }, diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json index 8d1c17efb32..f8c052bd5f8 100644 --- a/homeassistant/components/netatmo/translations/ko.json +++ b/homeassistant/components/netatmo/translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\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." }, diff --git a/homeassistant/components/netatmo/translations/lb.json b/homeassistant/components/netatmo/translations/lb.json index 86212bb5b9f..73032f898d3 100644 --- a/homeassistant/components/netatmo/translations/lb.json +++ b/homeassistant/components/netatmo/translations/lb.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "Scho konfigur\u00e9iert. N\u00ebmmen eng Konfiguratioun ass m\u00e9iglech.", "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL", "missing_configuration": "D\u00ebs Komponent ass net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." }, diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index 05257c29e11..67919938ee2 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_setup": "U kunt slechts \u00e9\u00e9n Netatmo account configureren." - }, "create_entry": { "default": "Succesvol geauthenticeerd met Netatmo." } diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json index a61c6576209..5cc1d400719 100644 --- a/homeassistant/components/netatmo/translations/no.json +++ b/homeassistant/components/netatmo/translations/no.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_setup": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "authorize_url_timeout": "Tidsavbrutt ved oppretting av godkjennings url.", - "missing_configuration": "Komponeneten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "missing_configuration": "Komponeneten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "create_entry": { "default": "Vellykket godkjenning med Netatmo." diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json index 864511eeb75..8a549e4cd30 100644 --- a/homeassistant/components/netatmo/translations/pl.json +++ b/homeassistant/components/netatmo/translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json index 4b124a75ca5..69a0430a4ba 100644 --- a/homeassistant/components/netatmo/translations/ru.json +++ b/homeassistant/components/netatmo/translations/ru.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\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." + "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.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/netatmo/translations/sl.json b/homeassistant/components/netatmo/translations/sl.json index 16c74bc0ea7..99e83367700 100644 --- a/homeassistant/components/netatmo/translations/sl.json +++ b/homeassistant/components/netatmo/translations/sl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "Konfigurirate lahko samo en ra\u010dun Netatmo.", "authorize_url_timeout": "Potekla je \u010dasovna omejitev generiranja odobritnevega URL-ja.", "missing_configuration": "Ta komponenta ni nastavljena, prosimo sledite dokumentaciji." }, diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index 365755b2806..37badeaab53 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_setup": "Du kan endast konfigurera ett Netatmo-konto." - }, "create_entry": { "default": "Autentiserad med Netatmo." } diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index 54f4e1a89e2..c54488cdcbc 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, diff --git a/homeassistant/components/ovo_energy/translations/ca.json b/homeassistant/components/ovo_energy/translations/ca.json new file mode 100644 index 00000000000..11ff423f1dd --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "authorization_error": "Error d'autoritzaci\u00f3. Comproveu les vostres credencials.", + "connection_error": "No s'ha pogut connectar a OVO Energy." + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Configura una inst\u00e0ncia OVO Energy per accedir al consum energ\u00e8tic.", + "title": "Afegir OVO Energy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json index afe1bb6e301..64a3bfe80c3 100644 --- a/homeassistant/components/ovo_energy/translations/en.json +++ b/homeassistant/components/ovo_energy/translations/en.json @@ -1,18 +1,18 @@ { - "config": { - "error": { - "authorization_error": "Authorization error. Check your credentials.", - "connection_error": "Could not connect to OVO Energy." - }, - "step": { - "user": { - "data": { - "username": "Username", - "password": "Password" + "config": { + "error": { + "authorization_error": "Authorization error. Check your credentials.", + "connection_error": "Could not connect to OVO Energy." }, - "description": "Set up an OVO Energy instance to access your energy usage.", - "title": "Add OVO Energy" - } + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Set up an OVO Energy instance to access your energy usage.", + "title": "Add OVO Energy" + } + } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json new file mode 100644 index 00000000000..50e1d74b0e9 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "authorization_error": "Autoriseringsfeil. Sjekk legitimasjonsbeskrivelsen.", + "connection_error": "Kunne ikke koble til OVO Energy." + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Sett opp en OVO Energy-forekomst for \u00e5 f\u00e5 tilgang til energibruken din.", + "title": "Legg til OVO Energy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json new file mode 100644 index 00000000000..cf69cb2973d --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "authorization_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a OVO Energy." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "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 OVO Energy.", + "title": "OVO Energy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index bdfd4e55937..15f9de8ee70 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -4,7 +4,7 @@ "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." }, "create_entry": { - "default": "\n\n\u00d6ffnen unter Android [die OwnTracks-App]({android_url}) und gehe zu {android_url} - > Verbindung. \u00c4nder die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: ` ` \n - Ger\u00e4te-ID: ` ` \n\n\u00d6ffnen unter iOS [die OwnTracks-App]({ios_url}) und tippe auf das Symbol (i) oben links - > Einstellungen. \u00c4nder die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: ` ` \n\n {secret} \n \n Weitere Informationen findest du in der [Dokumentation]({docs_url})." + "default": "\n\n\u00d6ffnen unter Android [die OwnTracks-App]({android_url}) und gehe zu {android_url} - > Verbindung. \u00c4nder die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: `''` \n - Ger\u00e4te-ID: `''` \n\n\u00d6ffnen unter iOS [die OwnTracks-App]({ios_url}) und tippe auf das Symbol (i) oben links - > Einstellungen. \u00c4nder die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: `''`\n\n {secret} \n \n Weitere Informationen findest du in der [Dokumentation]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index 134e635253b..1a8b91fad6d 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat", - "duplicated_name": "El nom ja existeix" + "already_configured": "El servei ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 91655e7245d..f8149d83b19 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service ist bereits konfiguriert", - "duplicated_name": "Name existiert bereits" + "already_configured": "Service ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung konnte nicht hergestellt werden" diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index e6d579cecbc..98ac63514b6 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is already configured", - "duplicated_name": "Name already existed" + "already_configured": "Service is already configured" }, "error": { "cannot_connect": "Failed to connect" diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index 08391a45f63..48708d68104 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "El servicio ya est\u00e1 configurado", - "duplicated_name": "El nombre ya existe" + "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json index a5aba11d76f..2e068cf0036 100644 --- a/homeassistant/components/pi_hole/translations/fr.json +++ b/homeassistant/components/pi_hole/translations/fr.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service d\u00e9j\u00e0 configur\u00e9", - "duplicated_name": "Le nom existe d\u00e9j\u00e0" + "already_configured": "Service d\u00e9j\u00e0 configur\u00e9" }, "error": { "cannot_connect": "Connexion impossible" diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json index 5b7329f31d6..b8a155e9374 100644 --- a/homeassistant/components/pi_hole/translations/it.json +++ b/homeassistant/components/pi_hole/translations/it.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", - "duplicated_name": "Il nome \u00e8 gi\u00e0 esistente" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" }, "error": { "cannot_connect": "Impossibile connettersi" diff --git a/homeassistant/components/pi_hole/translations/ko.json b/homeassistant/components/pi_hole/translations/ko.json index 0f057e9c7be..4653cc8564d 100644 --- a/homeassistant/components/pi_hole/translations/ko.json +++ b/homeassistant/components/pi_hole/translations/ko.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "duplicated_name": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/pi_hole/translations/lb.json b/homeassistant/components/pi_hole/translations/lb.json index 540462c889d..db41851b9fd 100644 --- a/homeassistant/components/pi_hole/translations/lb.json +++ b/homeassistant/components/pi_hole/translations/lb.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service ass scho konfigur\u00e9iert", - "duplicated_name": "Numm g\u00ebtt et schonn" + "already_configured": "Service ass scho konfigur\u00e9iert" }, "error": { "cannot_connect": "Feeler beim verbannen" diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json index 7c399fc9ae6..5340086233c 100644 --- a/homeassistant/components/pi_hole/translations/nl.json +++ b/homeassistant/components/pi_hole/translations/nl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service al geconfigureerd", - "duplicated_name": "Naam bestond al" + "already_configured": "Service al geconfigureerd" }, "error": { "cannot_connect": "Kon niet verbinden" diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index e8bdbd2d18d..387b6c0d1eb 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert", - "duplicated_name": "Navnet eksisterte allerede" + "already_configured": "Tjenesten er allerede konfigurert" }, "error": { "cannot_connect": "Tilkobling feilet" diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index f263736382b..aa35fc9d918 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana.", - "duplicated_name": "Nazwa ju\u017c istnieje." + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." diff --git a/homeassistant/components/pi_hole/translations/pt-BR.json b/homeassistant/components/pi_hole/translations/pt-BR.json index c268b1182ce..a09bc3c2dec 100644 --- a/homeassistant/components/pi_hole/translations/pt-BR.json +++ b/homeassistant/components/pi_hole/translations/pt-BR.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Servi\u00e7o j\u00e1 configurado", - "duplicated_name": "O nome j\u00e1 existe" + "already_configured": "Servi\u00e7o j\u00e1 configurado" }, "error": { "cannot_connect": "Falha ao conectar" diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json index ceb9e609d61..ee3fa80251d 100644 --- a/homeassistant/components/pi_hole/translations/ru.json +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "duplicated_name": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json index df1d3c44b6f..1a75757dcc6 100644 --- a/homeassistant/components/pi_hole/translations/zh-Hant.json +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "duplicated_name": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/plex/translations/bg.json b/homeassistant/components/plex/translations/bg.json index 92cd6f59819..dfc12080ec5 100644 --- a/homeassistant/components/plex/translations/bg.json +++ b/homeassistant/components/plex/translations/bg.json @@ -4,8 +4,6 @@ "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0432\u0435\u0447\u0435 \u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", "already_configured": "\u0422\u043e\u0437\u0438 Plex \u0441\u044a\u0440\u0432\u044a\u0440 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", - "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430", - "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u0435\u043d \u0438\u043c\u043f\u043e\u0440\u0442", "token_request_timeout": "\u0418\u0437\u0442\u0435\u0447\u0435 \u0432\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/plex/translations/ca.json b/homeassistant/components/plex/translations/ca.json index fe78770ed9b..be4b6215f8b 100644 --- a/homeassistant/components/plex/translations/ca.json +++ b/homeassistant/components/plex/translations/ca.json @@ -4,8 +4,6 @@ "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats", "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", "already_in_progress": "S'est\u00e0 configurant Plex", - "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", - "non-interactive": "Importaci\u00f3 no interactiva", "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del token.", "unknown": "Ha fallat per motiu desconegut" }, diff --git a/homeassistant/components/plex/translations/da.json b/homeassistant/components/plex/translations/da.json index 24e51410d00..d0d5db18a9c 100644 --- a/homeassistant/components/plex/translations/da.json +++ b/homeassistant/components/plex/translations/da.json @@ -4,8 +4,6 @@ "all_configured": "Alle linkede servere er allerede konfigureret", "already_configured": "Denne Plex-server er allerede konfigureret", "already_in_progress": "Plex konfigureres", - "invalid_import": "Importeret konfiguration er ugyldig", - "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Timeout ved hentning af token", "unknown": "Mislykkedes af ukendt \u00e5rsag" }, diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json index a762d7b3ab8..b14e3a3c574 100644 --- a/homeassistant/components/plex/translations/de.json +++ b/homeassistant/components/plex/translations/de.json @@ -4,8 +4,6 @@ "all_configured": "Alle verkn\u00fcpften Server sind bereits konfiguriert", "already_configured": "Dieser Plex-Server ist bereits konfiguriert", "already_in_progress": "Plex wird konfiguriert", - "invalid_import": "Die importierte Konfiguration ist ung\u00fcltig", - "non-interactive": "Nicht interaktiver Import", "token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens", "unknown": "Aus unbekanntem Grund fehlgeschlagen" }, diff --git a/homeassistant/components/plex/translations/en.json b/homeassistant/components/plex/translations/en.json index 78c250ccc37..83e5196fc35 100644 --- a/homeassistant/components/plex/translations/en.json +++ b/homeassistant/components/plex/translations/en.json @@ -4,8 +4,6 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", - "invalid_import": "Imported configuration is invalid", - "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" }, diff --git a/homeassistant/components/plex/translations/es-419.json b/homeassistant/components/plex/translations/es-419.json index 62f5d6dd6d5..96637b3795a 100644 --- a/homeassistant/components/plex/translations/es-419.json +++ b/homeassistant/components/plex/translations/es-419.json @@ -4,8 +4,6 @@ "all_configured": "Todos los servidores vinculados ya fueron configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", - "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", - "non-interactive": "Importaci\u00f3n no interactiva", "token_request_timeout": "Se agot\u00f3 el tiempo de espera para obtener el token", "unknown": "Fall\u00f3 por razones desconocidas" }, diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 6cdecd52408..907025590c6 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -4,8 +4,6 @@ "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", - "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", - "non-interactive": "Importaci\u00f3n no interactiva", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" }, diff --git a/homeassistant/components/plex/translations/fr.json b/homeassistant/components/plex/translations/fr.json index 49e402b9ff0..eea3f5f678a 100644 --- a/homeassistant/components/plex/translations/fr.json +++ b/homeassistant/components/plex/translations/fr.json @@ -4,8 +4,6 @@ "all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s", "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Plex en cours de configuration", - "invalid_import": "La configuration import\u00e9e est invalide", - "non-interactive": "Importation non interactive", "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" }, diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index 48a2da53998..cfccf5c83e6 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -4,8 +4,6 @@ "all_configured": "Az \u00f6sszes \u00f6sszekapcsolt szerver m\u00e1r konfigur\u00e1lva van", "already_configured": "Ez a Plex szerver m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A Plex konfigur\u00e1l\u00e1sa folyamatban van", - "invalid_import": "Az import\u00e1lt konfigur\u00e1ci\u00f3 \u00e9rv\u00e9nytelen", - "non-interactive": "Nem interakt\u00edv import\u00e1l\u00e1s", "token_request_timeout": "Token k\u00e9r\u00e9sre sz\u00e1nt id\u0151 lej\u00e1rt", "unknown": "Ismeretlen okb\u00f3l nem siker\u00fclt" }, diff --git a/homeassistant/components/plex/translations/it.json b/homeassistant/components/plex/translations/it.json index db0de6b3e13..b0996fad3d7 100644 --- a/homeassistant/components/plex/translations/it.json +++ b/homeassistant/components/plex/translations/it.json @@ -4,8 +4,6 @@ "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Plex \u00e8 in fase di configurazione", - "invalid_import": "La configurazione importata non \u00e8 valida", - "non-interactive": "Importazione non interattiva", "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" }, diff --git a/homeassistant/components/plex/translations/ko.json b/homeassistant/components/plex/translations/ko.json index e376913eef6..7c461fe1673 100644 --- a/homeassistant/components/plex/translations/ko.json +++ b/homeassistant/components/plex/translations/ko.json @@ -4,8 +4,6 @@ "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84", "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", - "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "non-interactive": "\ube44 \ub300\ud654\ud615 \uac00\uc838\uc624\uae30", "token_request_timeout": "\ud1a0\ud070 \ud68d\ub4dd \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/plex/translations/lb.json b/homeassistant/components/plex/translations/lb.json index aab4c8b061e..3a01e3f67c1 100644 --- a/homeassistant/components/plex/translations/lb.json +++ b/homeassistant/components/plex/translations/lb.json @@ -4,8 +4,6 @@ "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert", "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", - "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", - "non-interactive": "Net interaktiven Import", "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" }, diff --git a/homeassistant/components/plex/translations/nl.json b/homeassistant/components/plex/translations/nl.json index 142f5f8bec7..1d5a9351426 100644 --- a/homeassistant/components/plex/translations/nl.json +++ b/homeassistant/components/plex/translations/nl.json @@ -4,8 +4,6 @@ "all_configured": "Alle gekoppelde servers zijn al geconfigureerd", "already_configured": "Deze Plex-server is al geconfigureerd", "already_in_progress": "Plex wordt geconfigureerd", - "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", - "non-interactive": "Niet-interactieve import", "token_request_timeout": "Time-out verkrijgen van token", "unknown": "Mislukt om onbekende reden" }, diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json index ab6c8232985..c7374e27b60 100644 --- a/homeassistant/components/plex/translations/no.json +++ b/homeassistant/components/plex/translations/no.json @@ -4,8 +4,6 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Plex blir konfigurert", - "invalid_import": "Den importerte konfigurasjonen er ugyldig", - "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" }, diff --git a/homeassistant/components/plex/translations/pl.json b/homeassistant/components/plex/translations/pl.json index ce67ab36168..a1d4ee96a1b 100644 --- a/homeassistant/components/plex/translations/pl.json +++ b/homeassistant/components/plex/translations/pl.json @@ -4,8 +4,6 @@ "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", "already_configured": "Ten serwer Plex jest ju\u017c skonfigurowany.", "already_in_progress": "Plex jest konfigurowany", - "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", - "non-interactive": "Nieinteraktywny import", "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena.", "unknown": "Nieznany b\u0142\u0105d" }, diff --git a/homeassistant/components/plex/translations/pt-BR.json b/homeassistant/components/plex/translations/pt-BR.json index 5768214c1a1..cc965a12740 100644 --- a/homeassistant/components/plex/translations/pt-BR.json +++ b/homeassistant/components/plex/translations/pt-BR.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "non-interactive": "Importa\u00e7\u00e3o n\u00e3o interativa" - }, "error": { "ssl_error": "Problema no certificado SSL" }, diff --git a/homeassistant/components/plex/translations/pt.json b/homeassistant/components/plex/translations/pt.json index 5a16b72237d..81b70bcd082 100644 --- a/homeassistant/components/plex/translations/pt.json +++ b/homeassistant/components/plex/translations/pt.json @@ -3,7 +3,6 @@ "abort": { "already_configured": "Este servidor Plex j\u00e1 est\u00e1 configurado", "already_in_progress": "Plex est\u00e1 a ser configurado", - "invalid_import": "A configura\u00e7\u00e3o importada \u00e9 inv\u00e1lida", "unknown": "Falha por motivo desconhecido" }, "error": { diff --git a/homeassistant/components/plex/translations/ru.json b/homeassistant/components/plex/translations/ru.json index dbf2e6ab63f..29937ccea5f 100644 --- a/homeassistant/components/plex/translations/ru.json +++ b/homeassistant/components/plex/translations/ru.json @@ -4,8 +4,6 @@ "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", - "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430.", - "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439 \u0438\u043c\u043f\u043e\u0440\u0442.", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430.", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435." }, diff --git a/homeassistant/components/plex/translations/sl.json b/homeassistant/components/plex/translations/sl.json index 36471b43df8..5a7ae2db621 100644 --- a/homeassistant/components/plex/translations/sl.json +++ b/homeassistant/components/plex/translations/sl.json @@ -4,8 +4,6 @@ "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", "already_in_progress": "Plex se konfigurira", - "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", - "non-interactive": "Neinteraktivni uvoz", "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" }, diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index ef0af0c7090..5577be041e6 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -4,8 +4,6 @@ "all_configured": "Alla l\u00e4nkade servrar har redan konfigurerats", "already_configured": "Denna Plex-server \u00e4r redan konfigurerad", "already_in_progress": "Plex konfigureras", - "invalid_import": "Importerad konfiguration \u00e4r ogiltig", - "non-interactive": "Icke-interaktiv import", "token_request_timeout": "Timeout att erh\u00e5lla token", "unknown": "Misslyckades av ok\u00e4nd anledning" }, diff --git a/homeassistant/components/plex/translations/zh-Hant.json b/homeassistant/components/plex/translations/zh-Hant.json index 7e5ced6e034..2d866880dec 100644 --- a/homeassistant/components/plex/translations/zh-Hant.json +++ b/homeassistant/components/plex/translations/zh-Hant.json @@ -4,8 +4,6 @@ "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210", "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", - "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", - "non-interactive": "\u7121\u4e92\u52d5\u532f\u5165", "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" }, diff --git a/homeassistant/components/poolsense/translations/ca.json b/homeassistant/components/poolsense/translations/ca.json index e72d841efe8..58816c45a16 100644 --- a/homeassistant/components/poolsense/translations/ca.json +++ b/homeassistant/components/poolsense/translations/ca.json @@ -4,9 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index f32ed2e367b..d54c09e3cef 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, - "error": { - "cannot_connect": "Verbindung nicht m\u00f6glich", - "unknown": "Unerwarteter Fehler" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/poolsense/translations/en.json b/homeassistant/components/poolsense/translations/en.json index a38fca9ed48..524061fc023 100644 --- a/homeassistant/components/poolsense/translations/en.json +++ b/homeassistant/components/poolsense/translations/en.json @@ -4,9 +4,7 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "invalid_auth": "Invalid authentication" }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/es.json b/homeassistant/components/poolsense/translations/es.json index 97e714099e7..e2e5272f7ac 100644 --- a/homeassistant/components/poolsense/translations/es.json +++ b/homeassistant/components/poolsense/translations/es.json @@ -4,9 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unknown": "Error inesperado" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/fr.json b/homeassistant/components/poolsense/translations/fr.json index f891af06264..cd9949baa64 100644 --- a/homeassistant/components/poolsense/translations/fr.json +++ b/homeassistant/components/poolsense/translations/fr.json @@ -4,9 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "L'authentification ne'st pas valide", - "unknown": "Erreur inattendue" + "invalid_auth": "L'authentification ne'st pas valide" }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/it.json b/homeassistant/components/poolsense/translations/it.json index fe0046bb905..7198039c8b5 100644 --- a/homeassistant/components/poolsense/translations/it.json +++ b/homeassistant/components/poolsense/translations/it.json @@ -4,9 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida", - "unknown": "Errore imprevisto" + "invalid_auth": "Autenticazione non valida" }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/ko.json b/homeassistant/components/poolsense/translations/ko.json index 53fc9ca8fef..42a6654592f 100644 --- a/homeassistant/components/poolsense/translations/ko.json +++ b/homeassistant/components/poolsense/translations/ko.json @@ -4,9 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\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", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/lb.json b/homeassistant/components/poolsense/translations/lb.json index 5b91ee10c75..724cd804cc0 100644 --- a/homeassistant/components/poolsense/translations/lb.json +++ b/homeassistant/components/poolsense/translations/lb.json @@ -4,9 +4,7 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert" }, "error": { - "cannot_connect": "Feeler beim verbannen", - "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "unknown": "Onerwaarte Feeler" + "invalid_auth": "Ong\u00eblteg Authentifikatioun" }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/no.json b/homeassistant/components/poolsense/translations/no.json index edf3a8a9463..38adc04c1db 100644 --- a/homeassistant/components/poolsense/translations/no.json +++ b/homeassistant/components/poolsense/translations/no.json @@ -4,9 +4,7 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "cannot_connect": "Tilkobling mislyktes.", - "invalid_auth": "Ugyldig godkjenning", - "unknown": "Uventet feil" + "invalid_auth": "Ugyldig godkjenning" }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/pl.json b/homeassistant/components/poolsense/translations/pl.json index 29d7011f0c1..d463be1c5dd 100644 --- a/homeassistant/components/poolsense/translations/pl.json +++ b/homeassistant/components/poolsense/translations/pl.json @@ -4,9 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", - "unknown": "Nieoczekiwany b\u0142\u0105d." + "invalid_auth": "Niepoprawne uwierzytelnienie." }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/pt.json b/homeassistant/components/poolsense/translations/pt.json index 38d44d35e69..db5e24356b7 100644 --- a/homeassistant/components/poolsense/translations/pt.json +++ b/homeassistant/components/poolsense/translations/pt.json @@ -1,9 +1,7 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/ru.json b/homeassistant/components/poolsense/translations/ru.json index 5117e9c59aa..9c474d7d8e7 100644 --- a/homeassistant/components/poolsense/translations/ru.json +++ b/homeassistant/components/poolsense/translations/ru.json @@ -4,9 +4,7 @@ "already_configured": "\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." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/zh-Hans.json b/homeassistant/components/poolsense/translations/zh-Hans.json index f85f6c84eb1..a9947a056ff 100644 --- a/homeassistant/components/poolsense/translations/zh-Hans.json +++ b/homeassistant/components/poolsense/translations/zh-Hans.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "unknown": "\u672a\u77e5\u9519\u8bef" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/poolsense/translations/zh-Hant.json b/homeassistant/components/poolsense/translations/zh-Hant.json index 53d0748c906..f298fc17de1 100644 --- a/homeassistant/components/poolsense/translations/zh-Hant.json +++ b/homeassistant/components/poolsense/translations/zh-Hant.json @@ -4,9 +4,7 @@ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { "user": { diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json deleted file mode 100644 index a0ccd718ca4..00000000000 --- a/homeassistant/components/rfxtrx/translations/it.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "one": "uno", - "other": "altri" -} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/pt.json b/homeassistant/components/rfxtrx/translations/pt.json deleted file mode 100644 index 350ef57c286..00000000000 --- a/homeassistant/components/rfxtrx/translations/pt.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "one": "Um", - "other": "Outro" -} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/ca.json b/homeassistant/components/smappee/translations/ca.json index b34b7b86d6f..569c76518c2 100644 --- a/homeassistant/components/smappee/translations/ca.json +++ b/homeassistant/components/smappee/translations/ca.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Temps d'espera esgotat generant l'URL d'autoritzaci\u00f3.", - "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/cs.json b/homeassistant/components/smappee/translations/cs.json deleted file mode 100644 index 36ec311b7c3..00000000000 --- a/homeassistant/components/smappee/translations/cs.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "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/smappee/translations/en.json b/homeassistant/components/smappee/translations/en.json index fee105bf825..505d56e73a0 100644 --- a/homeassistant/components/smappee/translations/en.json +++ b/homeassistant/components/smappee/translations/en.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The component is not configured. Please follow the documentation.", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "missing_configuration": "The component is not configured. Please follow the documentation." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index f9b65b5339a..f67676e5165 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -2,8 +2,7 @@ "config": { "abort": { "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." + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/fr.json b/homeassistant/components/smappee/translations/fr.json index 1f90db7f30d..f231f1f1371 100644 --- a/homeassistant/components/smappee/translations/fr.json +++ b/homeassistant/components/smappee/translations/fr.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", - "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/it.json b/homeassistant/components/smappee/translations/it.json index 095557aeb5d..057bce6b714 100644 --- a/homeassistant/components/smappee/translations/it.json +++ b/homeassistant/components/smappee/translations/it.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/ko.json b/homeassistant/components/smappee/translations/ko.json index 6e6331f30f1..05557a6046d 100644 --- a/homeassistant/components/smappee/translations/ko.json +++ b/homeassistant/components/smappee/translations/ko.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\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.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\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." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/lb.json b/homeassistant/components/smappee/translations/lb.json index a95770a058f..8169e17a6de 100644 --- a/homeassistant/components/smappee/translations/lb.json +++ b/homeassistant/components/smappee/translations/lb.json @@ -2,8 +2,7 @@ "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.", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng Konfiguratioun ass m\u00e9iglech." + "missing_configuration": "Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/no.json b/homeassistant/components/smappee/translations/no.json index a6ef71b7448..6b2141fd61e 100644 --- a/homeassistant/components/smappee/translations/no.json +++ b/homeassistant/components/smappee/translations/no.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/pl.json b/homeassistant/components/smappee/translations/pl.json index 8f9f0d9803d..da5a481c22b 100644 --- a/homeassistant/components/smappee/translations/pl.json +++ b/homeassistant/components/smappee/translations/pl.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/ru.json b/homeassistant/components/smappee/translations/ru.json index abed7656da7..101e8d2fcec 100644 --- a/homeassistant/components/smappee/translations/ru.json +++ b/homeassistant/components/smappee/translations/ru.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\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.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + "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." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json index 3ff9da90cfb..8a93907cbb4 100644 --- a/homeassistant/components/smappee/translations/zh-Hant.json +++ b/homeassistant/components/smappee/translations/zh-Hant.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/spider/translations/ca.json b/homeassistant/components/spider/translations/ca.json new file mode 100644 index 00000000000..41d82da80cf --- /dev/null +++ b/homeassistant/components/spider/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/es.json b/homeassistant/components/spider/translations/es.json new file mode 100644 index 00000000000..230eb9f53c8 --- /dev/null +++ b/homeassistant/components/spider/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "title": "Iniciar sesi\u00f3n con la cuenta de mijn.ithodaalderop.nl" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/no.json b/homeassistant/components/spider/translations/no.json new file mode 100644 index 00000000000..934e7b686e9 --- /dev/null +++ b/homeassistant/components/spider/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Logg p\u00e5 med min.ithodaalderop.nl-konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/ru.json b/homeassistant/components/spider/translations/ru.json new file mode 100644 index 00000000000..983f2b94361 --- /dev/null +++ b/homeassistant/components/spider/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 mijn.ithodaalderop.nl" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/bg.json b/homeassistant/components/tellduslive/translations/bg.json index eef994a608e..903819c1f3e 100644 --- a/homeassistant/components/tellduslive/translations/bg.json +++ b/homeassistant/components/tellduslive/translations/bg.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "TelldusLive \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/tellduslive/translations/ca.json b/homeassistant/components/tellduslive/translations/ca.json index ae2bd468964..4326269131e 100644 --- a/homeassistant/components/tellduslive/translations/ca.json +++ b/homeassistant/components/tellduslive/translations/ca.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive ja est\u00e0 configurat", - "already_setup": "TelldusLive 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.", "unknown": "S'ha produ\u00eft un error desconegut" diff --git a/homeassistant/components/tellduslive/translations/da.json b/homeassistant/components/tellduslive/translations/da.json index 2ceb78b6bd0..c64e51dd06a 100644 --- a/homeassistant/components/tellduslive/translations/da.json +++ b/homeassistant/components/tellduslive/translations/da.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "TelldusLive er allerede konfigureret", "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", "authorize_url_timeout": "Timeout ved generering af autoriseret url.", "unknown": "Ukendt fejl opstod" diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json index 7d2ab21d81f..429ca47fbdf 100644 --- a/homeassistant/components/tellduslive/translations/de.json +++ b/homeassistant/components/tellduslive/translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "TelldusLive ist bereits konfiguriert", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "unknown": "Unbekannter Fehler ist aufgetreten" diff --git a/homeassistant/components/tellduslive/translations/en.json b/homeassistant/components/tellduslive/translations/en.json index 04bd2a192c1..ef509f0ac43 100644 --- a/homeassistant/components/tellduslive/translations/en.json +++ b/homeassistant/components/tellduslive/translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive is already configured", - "already_setup": "TelldusLive is already configured", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", "unknown": "Unknown error occurred" diff --git a/homeassistant/components/tellduslive/translations/es-419.json b/homeassistant/components/tellduslive/translations/es-419.json index 71529c1f41d..0deeb26eea6 100644 --- a/homeassistant/components/tellduslive/translations/es-419.json +++ b/homeassistant/components/tellduslive/translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "TelldusLive ya est\u00e1 configurado", "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "unknown": "Se produjo un error desconocido" diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index 378274f63af..3fcc57050fe 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive ya est\u00e1 configurado", - "already_setup": "TelldusLive ya est\u00e1 configurado", "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", "unknown": "Se produjo un error desconocido" diff --git a/homeassistant/components/tellduslive/translations/fr.json b/homeassistant/components/tellduslive/translations/fr.json index f3e90a5c7bf..a6c125fb04e 100644 --- a/homeassistant/components/tellduslive/translations/fr.json +++ b/homeassistant/components/tellduslive/translations/fr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive est d\u00e9j\u00e0 configur\u00e9", - "already_setup": "TelldusLive est d\u00e9j\u00e0 configur\u00e9", "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "unknown": "Une erreur inconnue s'est produite" diff --git a/homeassistant/components/tellduslive/translations/hu.json b/homeassistant/components/tellduslive/translations/hu.json index ace432f6a3f..c285fd8213d 100644 --- a/homeassistant/components/tellduslive/translations/hu.json +++ b/homeassistant/components/tellduslive/translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "A TelldusLive m\u00e1r be van \u00e1ll\u00edtva", "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/tellduslive/translations/it.json b/homeassistant/components/tellduslive/translations/it.json index e8f74f5ce29..c4afb96170e 100644 --- a/homeassistant/components/tellduslive/translations/it.json +++ b/homeassistant/components/tellduslive/translations/it.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive \u00e8 gi\u00e0 configurato", - "already_setup": "TelldusLive \u00e8 gi\u00e0 configurato", "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", "unknown": "Si \u00e8 verificato un errore sconosciuto." diff --git a/homeassistant/components/tellduslive/translations/ko.json b/homeassistant/components/tellduslive/translations/ko.json index ffbded23f7f..ef1b20086c5 100644 --- a/homeassistant/components/tellduslive/translations/ko.json +++ b/homeassistant/components/tellduslive/translations/ko.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "already_setup": "TelldusLive \uac00 \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.", "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/tellduslive/translations/lb.json b/homeassistant/components/tellduslive/translations/lb.json index bf91c5d26ea..ba200074ac5 100644 --- a/homeassistant/components/tellduslive/translations/lb.json +++ b/homeassistant/components/tellduslive/translations/lb.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive ass scho konfigur\u00e9iert", - "already_setup": "TelldusLive ass scho konfigur\u00e9iert", "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "unknown": "Onbekannten Feeler opgetrueden" diff --git a/homeassistant/components/tellduslive/translations/nl.json b/homeassistant/components/tellduslive/translations/nl.json index d17637e7c2c..1ef78ae1fd3 100644 --- a/homeassistant/components/tellduslive/translations/nl.json +++ b/homeassistant/components/tellduslive/translations/nl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "TelldusLive is al geconfigureerd", "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie url.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "unknown": "Onbekende fout opgetreden" diff --git a/homeassistant/components/tellduslive/translations/no.json b/homeassistant/components/tellduslive/translations/no.json index 8c2f3d3489d..232db7a0fad 100644 --- a/homeassistant/components/tellduslive/translations/no.json +++ b/homeassistant/components/tellduslive/translations/no.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive er allerede konfigurert", - "already_setup": "TelldusLive er allerede konfigurert", "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", "unknown": "Ukjent feil oppstod" diff --git a/homeassistant/components/tellduslive/translations/pl.json b/homeassistant/components/tellduslive/translations/pl.json index deee22d9ed8..8145717b40e 100644 --- a/homeassistant/components/tellduslive/translations/pl.json +++ b/homeassistant/components/tellduslive/translations/pl.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive jest ju\u017c skonfigurowany", - "already_setup": "TelldusLive jest ju\u017c skonfigurowany.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "unknown": "Nieoczekiwany b\u0142\u0105d." diff --git a/homeassistant/components/tellduslive/translations/pt-BR.json b/homeassistant/components/tellduslive/translations/pt-BR.json index a1114965a4b..a63d94eb25b 100644 --- a/homeassistant/components/tellduslive/translations/pt-BR.json +++ b/homeassistant/components/tellduslive/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "TelldusLive j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo limite de gera\u00e7\u00e3o de url de autoriza\u00e7\u00e3o.", "unknown": "Ocorreu um erro desconhecido" diff --git a/homeassistant/components/tellduslive/translations/pt.json b/homeassistant/components/tellduslive/translations/pt.json index 549fa406253..ac374ba7b2c 100644 --- a/homeassistant/components/tellduslive/translations/pt.json +++ b/homeassistant/components/tellduslive/translations/pt.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "TelldusLive j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", "unknown": "Ocorreu um erro desconhecido" diff --git a/homeassistant/components/tellduslive/translations/ru.json b/homeassistant/components/tellduslive/translations/ru.json index 4aac3b2a5d5..c1d052930a1 100644 --- a/homeassistant/components/tellduslive/translations/ru.json +++ b/homeassistant/components/tellduslive/translations/ru.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "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.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/tellduslive/translations/sl.json b/homeassistant/components/tellduslive/translations/sl.json index 0775e47f694..a0634b3c6e5 100644 --- a/homeassistant/components/tellduslive/translations/sl.json +++ b/homeassistant/components/tellduslive/translations/sl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "TelldusLive je \u017ee konfiguriran", "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje URL-ja je potekla.", "unknown": "Pri\u0161lo je do neznane napake" diff --git a/homeassistant/components/tellduslive/translations/sv.json b/homeassistant/components/tellduslive/translations/sv.json index 1a8affa7c31..96e1258faa5 100644 --- a/homeassistant/components/tellduslive/translations/sv.json +++ b/homeassistant/components/tellduslive/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "Telldus Live! \u00e4r redan konfigurerad", "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r genererar en url f\u00f6r att auktorisera.", "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", "unknown": "Ok\u00e4nt fel intr\u00e4ffade" diff --git a/homeassistant/components/tellduslive/translations/zh-Hans.json b/homeassistant/components/tellduslive/translations/zh-Hans.json index f28bb62b10a..c1d4a0c54f5 100644 --- a/homeassistant/components/tellduslive/translations/zh-Hans.json +++ b/homeassistant/components/tellduslive/translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_setup": "TelldusLive \u5df2\u914d\u7f6e\u5b8c\u6210", "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", "unknown": "\u53d1\u751f\u672a\u77e5\u7684\u9519\u8bef" diff --git a/homeassistant/components/tellduslive/translations/zh-Hant.json b/homeassistant/components/tellduslive/translations/zh-Hant.json index 0683c783677..de64974b2de 100644 --- a/homeassistant/components/tellduslive/translations/zh-Hant.json +++ b/homeassistant/components/tellduslive/translations/zh-Hant.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "TelldusLive \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_setup": "TelldusLive \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "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", "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" diff --git a/homeassistant/components/toon/translations/bg.json b/homeassistant/components/toon/translations/bg.json index 0108f532d6a..11793c5bb5d 100644 --- a/homeassistant/components/toon/translations/bg.json +++ b/homeassistant/components/toon/translations/bg.json @@ -1,33 +1,7 @@ { "config": { "abort": { - "client_id": "\u041a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u044f\u0442 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043e\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d.", - "client_secret": "\u041a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0430\u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u0430 \u043e\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430.", - "no_agreements": "\u0422\u043e\u0437\u0438 \u043f\u0440\u043e\u0444\u0438\u043b \u043d\u044f\u043c\u0430 Toon \u0434\u0438\u0441\u043f\u043b\u0435\u0438.", - "no_app": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Toon, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0441\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0442\u0435. [\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438\u0442\u0435] (https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." - }, - "error": { - "credentials": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u0441\u0430 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438.", - "display_exists": "\u0418\u0437\u0431\u0440\u0430\u043d\u0438\u044f\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d." - }, - "step": { - "authenticate": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "tenant": "\u041d\u0430\u0435\u043c\u0430\u0442\u0435\u043b", - "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" - }, - "description": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f Eneco Toon \u043f\u0440\u043e\u0444\u0438\u043b (\u043d\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0437\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u0446\u0438).", - "title": "\u0421\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0412\u0430\u0448\u0438\u044f \u0430\u043a\u0430\u0443\u043d\u0442 \u0432 \u0422\u043e\u043e\u043d" - }, - "display": { - "data": { - "display": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u0439" - }, - "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u043d\u0430 Toon, \u0441 \u043a\u043e\u0439\u0442\u043e \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435.", - "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u0439" - } + "no_agreements": "\u0422\u043e\u0437\u0438 \u043f\u0440\u043e\u0444\u0438\u043b \u043d\u044f\u043c\u0430 Toon \u0434\u0438\u0441\u043f\u043b\u0435\u0438." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/ca.json b/homeassistant/components/toon/translations/ca.json index 8b424b3d647..3b25d54b974 100644 --- a/homeassistant/components/toon/translations/ca.json +++ b/homeassistant/components/toon/translations/ca.json @@ -4,16 +4,8 @@ "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": "Temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3 esgotat.", - "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." - }, - "error": { - "credentials": "Les credencials proporcionades no s\u00f3n v\u00e0lides.", - "display_exists": "La pantalla seleccionada ja est\u00e0 configurada." + "no_agreements": "Aquest compte no t\u00e9 pantalles Toon." }, "step": { "agreement": { @@ -23,22 +15,6 @@ "description": "Seleccioneu l'adre\u00e7a de l'acord a afegir.", "title": "Selecciona acord" }, - "authenticate": { - "data": { - "password": "Contrasenya", - "tenant": "Tenant", - "username": "Nom d'usuari" - }, - "description": "Autentica't amb el teu compte d'Eneco Toon (no el compte de desenvolupador).", - "title": "Enlla\u00e7ar compte de Toon" - }, - "display": { - "data": { - "display": "Tria la visualitzaci\u00f3" - }, - "description": "Selecciona la pantalla Toon amb la qual vols connectar-te.", - "title": "Selecci\u00f3 de pantalla" - }, "pick_implementation": { "title": "Tria amb quin vols autenticar-te" } diff --git a/homeassistant/components/toon/translations/cs.json b/homeassistant/components/toon/translations/cs.json index ad1cdd5e76f..e514edf63aa 100644 --- a/homeassistant/components/toon/translations/cs.json +++ b/homeassistant/components/toon/translations/cs.json @@ -8,12 +8,6 @@ "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." } diff --git a/homeassistant/components/toon/translations/da.json b/homeassistant/components/toon/translations/da.json index 73d18f22911..37215949d6f 100644 --- a/homeassistant/components/toon/translations/da.json +++ b/homeassistant/components/toon/translations/da.json @@ -1,33 +1,7 @@ { "config": { "abort": { - "client_id": "Klient-id'et fra konfigurationen er ugyldigt.", - "client_secret": "Klientens hemmelighed fra konfigurationen er ugyldig.", - "no_agreements": "Denne konto har ingen Toon-sk\u00e6rme.", - "no_app": "Du skal konfigurere Toon f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Der opstod en uventet fejl under godkendelse." - }, - "error": { - "credentials": "De angivne legitimationsoplysninger er ugyldige.", - "display_exists": "Den valgte sk\u00e6rm er allerede konfigureret." - }, - "step": { - "authenticate": { - "data": { - "password": "Adgangskode", - "tenant": "Tenant", - "username": "Brugernavn" - }, - "description": "Godkend med din Eneco Toon-konto (ikke udviklerkontoen).", - "title": "Forbind din Toon-konto" - }, - "display": { - "data": { - "display": "V\u00e6lg sk\u00e6rm" - }, - "description": "V\u00e6lg den Toon sk\u00e6rm, du vil oprette forbindelse til.", - "title": "V\u00e6lg sk\u00e6rm" - } + "no_agreements": "Denne konto har ingen Toon-sk\u00e6rme." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json index 0c4aee8cdbf..4f4dd8a0956 100644 --- a/homeassistant/components/toon/translations/de.json +++ b/homeassistant/components/toon/translations/de.json @@ -1,33 +1,7 @@ { "config": { "abort": { - "client_id": "Die Client-ID aus der Konfiguration ist ung\u00fcltig.", - "client_secret": "Das Client-Secret aus der Konfiguration ist ung\u00fcltig.", - "no_agreements": "Dieses Konto hat keine Toon-Anzeigen.", - "no_app": "Toon muss konfiguriert werden, bevor die Authentifizierung durchgef\u00fchrt werden kann. [Lies bitte die Anleitung](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Beim Authentifizieren ist ein unerwarteter Fehler aufgetreten." - }, - "error": { - "credentials": "Die angegebenen Anmeldeinformationen sind ung\u00fcltig.", - "display_exists": "Die ausgew\u00e4hlte Anzeige ist bereits konfiguriert." - }, - "step": { - "authenticate": { - "data": { - "password": "Passwort", - "tenant": "Tenant", - "username": "Benutzername" - }, - "description": "Authentifiziere dich mit deinem Eneco Toon-Konto (nicht dem Entwicklerkonto).", - "title": "Verkn\u00fcpfe dein Toon-Konto" - }, - "display": { - "data": { - "display": "Anzeige w\u00e4hlen" - }, - "description": "W\u00e4hle die Toon-Anzeige aus, die verbunden werden soll.", - "title": "Anzeige ausw\u00e4hlen" - } + "no_agreements": "Dieses Konto hat keine Toon-Anzeigen." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/en.json b/homeassistant/components/toon/translations/en.json index 0f0b5c5598b..b15caa77aba 100644 --- a/homeassistant/components/toon/translations/en.json +++ b/homeassistant/components/toon/translations/en.json @@ -4,16 +4,8 @@ "already_configured": "The selected agreement is already configured.", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize URL.", - "client_id": "The client ID from the configuration is invalid.", - "client_secret": "The client secret from the configuration is invalid.", "missing_configuration": "The component is not configured. Please follow the documentation.", - "no_agreements": "This account has no Toon displays.", - "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Unexpected error occurred, while authenticating." - }, - "error": { - "credentials": "The provided credentials are invalid.", - "display_exists": "The selected display is already configured." + "no_agreements": "This account has no Toon displays." }, "step": { "agreement": { @@ -23,22 +15,6 @@ "description": "Select the agreement address you want to add.", "title": "Select your agreement" }, - "authenticate": { - "data": { - "password": "Password", - "tenant": "Tenant", - "username": "Username" - }, - "description": "Authenticate with your Eneco Toon account (not the developer account).", - "title": "Link your Toon account" - }, - "display": { - "data": { - "display": "Choose display" - }, - "description": "Select the Toon display to connect with.", - "title": "Select display" - }, "pick_implementation": { "title": "Choose your tenant to authenticate with" } diff --git a/homeassistant/components/toon/translations/es-419.json b/homeassistant/components/toon/translations/es-419.json index af39c29971d..3a8ca204d2e 100644 --- a/homeassistant/components/toon/translations/es-419.json +++ b/homeassistant/components/toon/translations/es-419.json @@ -1,33 +1,7 @@ { "config": { "abort": { - "client_id": "La identificaci\u00f3n del cliente de la configuraci\u00f3n no es v\u00e1lida.", - "client_secret": "El secreto del cliente de la configuraci\u00f3n no es v\u00e1lido.", - "no_agreements": "Esta cuenta no tiene pantallas Toon.", - "no_app": "Debe configurar Toon antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Ocurri\u00f3 un error inesperado, mientras se autenticaba." - }, - "error": { - "credentials": "Las credenciales proporcionadas no son v\u00e1lidas.", - "display_exists": "La pantalla seleccionada ya est\u00e1 configurada." - }, - "step": { - "authenticate": { - "data": { - "password": "Contrase\u00f1a", - "tenant": "Tenant", - "username": "Nombre de usuario" - }, - "description": "Autent\u00edquese con su cuenta de Eneco Toon (no con la cuenta de desarrollador).", - "title": "Vincula tu cuenta de Toon" - }, - "display": { - "data": { - "display": "Elegir pantalla" - }, - "description": "Seleccione la pantalla Toon para conectarse.", - "title": "Seleccionar pantalla" - } + "no_agreements": "Esta cuenta no tiene pantallas Toon." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index 4861abd8b92..28e8a1dcb61 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -4,16 +4,8 @@ "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." - }, - "error": { - "credentials": "Las credenciales proporcionadas no son v\u00e1lidas.", - "display_exists": "La pantalla seleccionada ya est\u00e1 configurada." + "no_agreements": "Esta cuenta no tiene pantallas Toon." }, "step": { "agreement": { @@ -23,22 +15,6 @@ "description": "Selecciona la direcci\u00f3n del acuerdo que deseas a\u00f1adir.", "title": "Selecciona tu acuerdo" }, - "authenticate": { - "data": { - "password": "Contrase\u00f1a", - "tenant": "Inquilino", - "username": "Usuario" - }, - "description": "Identif\u00edcate con tu cuenta de Eneco Toon (no con la cuenta de desarrollador).", - "title": "Vincular tu cuenta Toon" - }, - "display": { - "data": { - "display": "Elige una pantalla" - }, - "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/fi.json b/homeassistant/components/toon/translations/fi.json deleted file mode 100644 index e269670b21b..00000000000 --- a/homeassistant/components/toon/translations/fi.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "step": { - "authenticate": { - "title": "Yhdist\u00e4 Toon-tili" - }, - "display": { - "data": { - "display": "Valitse n\u00e4ytt\u00f6" - }, - "title": "Valitse n\u00e4ytt\u00f6" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/fr.json b/homeassistant/components/toon/translations/fr.json index ec466a4745c..c3384f56319 100644 --- a/homeassistant/components/toon/translations/fr.json +++ b/homeassistant/components/toon/translations/fr.json @@ -4,16 +4,8 @@ "already_configured": "L'accord s\u00e9lectionn\u00e9 est d\u00e9j\u00e0 configur\u00e9.", "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "Timout de g\u00e9n\u00e9ration de l'URL d'autorisation.", - "client_id": "L'ID client de la configuration n'est pas valide.", - "client_secret": "Le client secret de la configuration n'est pas valide.", "missing_configuration": "The composant n'est pas configur\u00e9. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation.", - "no_agreements": "Ce compte n'a pas d'affichages Toon.", - "no_app": "Vous devez configurer Toon avant de pouvoir vous authentifier avec celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Une erreur inattendue s'est produite lors de l'authentification." - }, - "error": { - "credentials": "Les informations d'identification fournies ne sont pas valides.", - "display_exists": "L'affichage s\u00e9lectionn\u00e9 est d\u00e9j\u00e0 configur\u00e9." + "no_agreements": "Ce compte n'a pas d'affichages Toon." }, "step": { "agreement": { @@ -23,22 +15,6 @@ "description": "S\u00e9lectionnez l'adresse d'accord que vous souhaitez ajouter.", "title": "S\u00e9lectionnez votre accord" }, - "authenticate": { - "data": { - "password": "Mot de passe", - "tenant": "Locataire", - "username": "Nom d'utilisateur" - }, - "description": "Authentifiez-vous avec votre compte Eneco Toon (pas le compte d\u00e9veloppeur).", - "title": "Lier un compte Toon" - }, - "display": { - "data": { - "display": "Choisissez l'affichage" - }, - "description": "S\u00e9lectionnez l'affichage Toon avec lequel vous connecter.", - "title": "S\u00e9lectionnez l'affichage" - }, "pick_implementation": { "title": "Choisissez votre locataire pour vous authentifier" } diff --git a/homeassistant/components/toon/translations/hu.json b/homeassistant/components/toon/translations/hu.json deleted file mode 100644 index 740e4bd381d..00000000000 --- a/homeassistant/components/toon/translations/hu.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "authenticate": { - "data": { - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json index 5e49e13bd7e..ed905ff899f 100644 --- a/homeassistant/components/toon/translations/it.json +++ b/homeassistant/components/toon/translations/it.json @@ -4,16 +4,8 @@ "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." - }, - "error": { - "credentials": "Le credenziali fornite non sono valide.", - "display_exists": "Il display selezionato \u00e8 gi\u00e0 configurato." + "no_agreements": "Questo account non ha display Toon." }, "step": { "agreement": { @@ -23,22 +15,6 @@ "description": "Selezionare l'indirizzo del contratto che si desidera aggiungere.", "title": "Seleziona il tuo contratto" }, - "authenticate": { - "data": { - "password": "Password", - "tenant": "Inquilino", - "username": "Nome utente" - }, - "description": "Autenticati con il tuo account Eneco Toon (non l'account sviluppatore).", - "title": "Collega il tuo account Toon" - }, - "display": { - "data": { - "display": "Seleziona il display" - }, - "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/ko.json b/homeassistant/components/toon/translations/ko.json index c932126b9ed..bebd8bb912e 100644 --- a/homeassistant/components/toon/translations/ko.json +++ b/homeassistant/components/toon/translations/ko.json @@ -4,16 +4,8 @@ "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." - }, - "error": { - "credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "display_exists": "\uc120\ud0dd\ub41c \ub514\uc2a4\ud50c\ub808\uc774\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { "agreement": { @@ -23,22 +15,6 @@ "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", - "tenant": "\uac70\uc8fc\uc790", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, - "description": "Eneco Toon \uacc4\uc815\uc73c\ub85c \uc778\uc99d\ud574\uc8fc\uc138\uc694. (\uac1c\ubc1c\uc790 \uacc4\uc815 \uc544\ub2d8)", - "title": "Toon \uacc4\uc815 \uc5f0\uacb0\ud558\uae30" - }, - "display": { - "data": { - "display": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd" - }, - "description": "\uc5f0\uacb0\ud560 Toon \ub514\uc2a4\ud50c\ub808\uc774\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", - "title": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd\ud558\uae30" - }, "pick_implementation": { "title": "\uc778\uc99d \ub300\uc0c1 \uc0ac\uc6a9\uc790 \uc120\ud0dd\ud558\uae30" } diff --git a/homeassistant/components/toon/translations/lb.json b/homeassistant/components/toon/translations/lb.json index 9c0d3711574..5d7095d0c85 100644 --- a/homeassistant/components/toon/translations/lb.json +++ b/homeassistant/components/toon/translations/lb.json @@ -4,16 +4,8 @@ "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." - }, - "error": { - "credentials": "Ong\u00eblteg Login Informatioune.", - "display_exists": "Den ausgewielten Ecran ass scho konfigur\u00e9iert." + "no_agreements": "D\u00ebse Kont huet keen Toon Ecran." }, "step": { "agreement": { @@ -23,22 +15,6 @@ "description": "Wiel d'Adress vum Accord aus dee dob\u00e4igesaat soll ginn.", "title": "D\u00e4in Accord auswielen" }, - "authenticate": { - "data": { - "password": "Passwuert", - "tenant": "Notzer", - "username": "Benotzernumm" - }, - "description": "Authentifikatioun mat \u00e4rem Eneco Toon Kont (net de Kont vum Entw\u00e9ckler)", - "title": "Toon Kont verbannnen" - }, - "display": { - "data": { - "display": "Ecran auswielen" - }, - "description": "Wielt den Toon Ecran aus fir sech domat ze verbannen.", - "title": "Ecran auswielen" - }, "pick_implementation": { "title": "Wielt de Locataire aus mat deem sech authentifiz\u00e9iert g\u00ebtt" } diff --git a/homeassistant/components/toon/translations/lt.json b/homeassistant/components/toon/translations/lt.json deleted file mode 100644 index 4c2802218f2..00000000000 --- a/homeassistant/components/toon/translations/lt.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "authenticate": { - "data": { - "password": "Slapta\u017eodis" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json index bcced85e29d..69eabaaf28b 100644 --- a/homeassistant/components/toon/translations/nl.json +++ b/homeassistant/components/toon/translations/nl.json @@ -1,33 +1,7 @@ { "config": { "abort": { - "client_id": "De client ID uit de configuratie is ongeldig.", - "client_secret": "De client secret uit de configuratie is ongeldig.", - "no_agreements": "Dit account heeft geen Toon schermen.", - "no_app": "Je moet Toon configureren voordat je ermee kunt aanmelden. [Lees de instructies](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Onverwachte fout tijdens het verifi\u00ebren." - }, - "error": { - "credentials": "De opgegeven inloggegevens zijn ongeldig.", - "display_exists": "Het gekozen scherm is al geconfigureerd." - }, - "step": { - "authenticate": { - "data": { - "password": "Wachtwoord", - "tenant": "Huurder", - "username": "Gebruikersnaam" - }, - "description": "Verifieer met je Eneco Toon account (niet het ontwikkelaars account).", - "title": "Link je Toon-account" - }, - "display": { - "data": { - "display": "Kies scherm" - }, - "description": "Kies het Toon-scherm om mee te verbinden.", - "title": "Kies scherm" - } + "no_agreements": "Dit account heeft geen Toon schermen." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/nn.json b/homeassistant/components/toon/translations/nn.json deleted file mode 100644 index b9bb31d468b..00000000000 --- a/homeassistant/components/toon/translations/nn.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "authenticate": { - "data": { - "username": "Brukarnamn" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json index b67f7386a22..37652c4aee1 100644 --- a/homeassistant/components/toon/translations/no.json +++ b/homeassistant/components/toon/translations/no.json @@ -4,16 +4,8 @@ "already_configured": "Den valgte avtalen er allerede konfigurert.", "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "client_id": "Klient ID fra konfigurasjonen er ugyldig.", - "client_secret": "Klient hemmeligheten fra konfigurasjonen er ugyldig.", "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", - "no_agreements": "Denne kontoen har ingen Toon skjermer.", - "no_app": "Du m\u00e5 konfigurere Toon f\u00f8r du kan autentisere den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Det oppstod en uventet feil under godkjenning." - }, - "error": { - "credentials": "De oppgitte legitimasjonene er ugyldige.", - "display_exists": "Den valgte skjermen er allerede konfigurert." + "no_agreements": "Denne kontoen har ingen Toon skjermer." }, "step": { "agreement": { @@ -23,22 +15,6 @@ "description": "Velg avtalsadressen du vil legge til.", "title": "Velg din avtale" }, - "authenticate": { - "data": { - "password": "Passord", - "tenant": "Leietaker", - "username": "Brukernavn" - }, - "description": "Godkjenn med Eneco Toon kontoen din (ikke utviklerkontoen).", - "title": "Koble til din Toon konto" - }, - "display": { - "data": { - "display": "Velg skjerm" - }, - "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/toon/translations/pl.json b/homeassistant/components/toon/translations/pl.json index 43dc3d635c4..dba8c957a3b 100644 --- a/homeassistant/components/toon/translations/pl.json +++ b/homeassistant/components/toon/translations/pl.json @@ -2,38 +2,14 @@ "config": { "abort": { "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "client_id": "Identyfikator klienta z konfiguracji jest nieprawid\u0142owy.", - "client_secret": "Tajny klucz klienta z konfiguracji jest nieprawid\u0142owy.", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", - "no_agreements": "To konto nie posiada wy\u015bwietlaczy Toon.", - "no_app": "Musisz skonfigurowa\u0107 Toon, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Nieoczekiwany b\u0142\u0105d podczas uwierzytelniania." - }, - "error": { - "credentials": "Wprowadzone dane logowania s\u0105 nieprawid\u0142owe.", - "display_exists": "Wybrany ekran jest ju\u017c skonfigurowany." + "no_agreements": "To konto nie posiada wy\u015bwietlaczy Toon." }, "step": { "agreement": { "data": { "agreement": "Umowa" } - }, - "authenticate": { - "data": { - "password": "Has\u0142o", - "tenant": "Najemca", - "username": "Nazwa u\u017cytkownika" - }, - "description": "Uwierzytelnij konto Eneco Toon (nie konto programisty).", - "title": "Po\u0142\u0105cz konto Toon" - }, - "display": { - "data": { - "display": "Wybierz wy\u015bwietlacz" - }, - "description": "Wybierz wy\u015bwietlacz Toon, z kt\u00f3rym chcesz si\u0119 po\u0142\u0105czy\u0107.", - "title": "Wybierz wy\u015bwietlacz" } } } diff --git a/homeassistant/components/toon/translations/pt-BR.json b/homeassistant/components/toon/translations/pt-BR.json index 3e6829f6596..2e12ac49a8a 100644 --- a/homeassistant/components/toon/translations/pt-BR.json +++ b/homeassistant/components/toon/translations/pt-BR.json @@ -1,33 +1,7 @@ { "config": { "abort": { - "client_id": "O ID do cliente da configura\u00e7\u00e3o \u00e9 inv\u00e1lido.", - "client_secret": "O segredo do cliente da configura\u00e7\u00e3o \u00e9 inv\u00e1lido.", - "no_agreements": "Esta conta n\u00e3o possui exibi\u00e7\u00f5es Toon.", - "no_app": "Voc\u00ea precisa configurar o Toon antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Ocorreu um erro inesperado durante a autentica\u00e7\u00e3o." - }, - "error": { - "credentials": "As credenciais fornecidas s\u00e3o inv\u00e1lidas.", - "display_exists": "A exibi\u00e7\u00e3o selecionada j\u00e1 est\u00e1 configurada." - }, - "step": { - "authenticate": { - "data": { - "password": "Senha", - "tenant": "Inquilino", - "username": "Usu\u00e1rio" - }, - "description": "Autentique-se com sua conta Eneco Toon (n\u00e3o com a conta do desenvolvedor).", - "title": "Vincule sua conta Toon" - }, - "display": { - "data": { - "display": "Escolha a exibi\u00e7\u00e3o" - }, - "description": "Selecione a exibi\u00e7\u00e3o Toon para se conectar.", - "title": "Selecione a exibi\u00e7\u00e3o" - } + "no_agreements": "Esta conta n\u00e3o possui exibi\u00e7\u00f5es Toon." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/pt.json b/homeassistant/components/toon/translations/pt.json index 8ef1e99d12c..9ecaef216f9 100644 --- a/homeassistant/components/toon/translations/pt.json +++ b/homeassistant/components/toon/translations/pt.json @@ -2,13 +2,7 @@ "config": { "abort": { "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", - "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", - "client_id": "O ID do cliente da configura\u00e7\u00e3o \u00e9 inv\u00e1lido.", - "client_secret": "O segredo do cliente da configura\u00e7\u00e3o \u00e9 inv\u00e1lido.", - "unknown_auth_fail": "Ocorreu um erro inesperado durante a autentica\u00e7\u00e3o." - }, - "error": { - "credentials": "As credenciais fornecidas s\u00e3o inv\u00e1lidas." + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" }, "step": { "agreement": { @@ -16,16 +10,6 @@ "agreement": "Acordo" }, "title": "Seleccione o seu acordo" - }, - "authenticate": { - "data": { - "password": "Palavra-passe", - "tenant": "Inquilino", - "username": "Nome de Utilizador" - } - }, - "display": { - "title": "Seleccionar visor" } } } diff --git a/homeassistant/components/toon/translations/ru.json b/homeassistant/components/toon/translations/ru.json index 7bdaf0eb08e..b5ae66ee54e 100644 --- a/homeassistant/components/toon/translations/ru.json +++ b/homeassistant/components/toon/translations/ru.json @@ -4,16 +4,8 @@ "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." - }, - "error": { - "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "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." + "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." }, "step": { "agreement": { @@ -23,22 +15,6 @@ "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", - "tenant": "\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446", - "username": "\u041b\u043e\u0433\u0438\u043d" - }, - "description": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Eneco Toon (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0451\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430).", - "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Toon" - }, - "display": { - "data": { - "display": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u0439" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 Toon \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", - "title": "Toon" - }, "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/toon/translations/sl.json b/homeassistant/components/toon/translations/sl.json index f86289d5a08..1883a5ab055 100644 --- a/homeassistant/components/toon/translations/sl.json +++ b/homeassistant/components/toon/translations/sl.json @@ -1,33 +1,7 @@ { "config": { "abort": { - "client_id": "ID odjemalca iz konfiguracije je neveljaven.", - "client_secret": "Skrivnost iz konfiguracije odjemalca ni veljaven.", - "no_agreements": "Ta ra\u010dun nima prikazov Toon.", - "no_app": "Toon morate konfigurirati, preden ga boste lahko uporabili za overitev. [Preberite navodila] (https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Med preverjanjem pristnosti je pri\u0161lo do nepri\u010dakovane napake." - }, - "error": { - "credentials": "Navedene poverilnice niso veljavne.", - "display_exists": "Izbrani zaslon je \u017ee konfiguriran." - }, - "step": { - "authenticate": { - "data": { - "password": "Geslo", - "tenant": "Najemnik", - "username": "Uporabni\u0161ko ime" - }, - "description": "Prijavite se s svojim Eneco toon ra\u010dunom (ne razvijalskim).", - "title": "Pove\u017eite svoj Toon ra\u010dun" - }, - "display": { - "data": { - "display": "Izberite zaslon" - }, - "description": "Izberite zaslon Toon, s katerim se \u017eelite povezati.", - "title": "Izberite zaslon" - } + "no_agreements": "Ta ra\u010dun nima prikazov Toon." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/sv.json b/homeassistant/components/toon/translations/sv.json index bb1c89e1e3c..034f5f41e68 100644 --- a/homeassistant/components/toon/translations/sv.json +++ b/homeassistant/components/toon/translations/sv.json @@ -1,33 +1,7 @@ { "config": { "abort": { - "client_id": "Client ID fr\u00e5n konfiguration \u00e4r ogiltig.", - "client_secret": "Client secret fr\u00e5n konfigurationen \u00e4r ogiltig.", - "no_agreements": "Det h\u00e4r kontot har inga Toon-sk\u00e4rmar.", - "no_app": "Du m\u00e5ste konfigurera Toon innan du kan autentisera med den. [L\u00e4s instruktioner] (https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Ov\u00e4ntat fel uppstod under autentisering." - }, - "error": { - "credentials": "De angivna uppgifterna \u00e4r ogiltiga.", - "display_exists": "Den valda sk\u00e4rmen \u00e4r redan konfigurerad" - }, - "step": { - "authenticate": { - "data": { - "password": "L\u00f6senord", - "tenant": "Hyresg\u00e4st", - "username": "Anv\u00e4ndarnamn" - }, - "description": "Autentisera med ditt Eneco Toon-konto (inte developer-kontot).", - "title": "L\u00e4nk ditt Toon-konto" - }, - "display": { - "data": { - "display": "V\u00e4lj sk\u00e4rm" - }, - "description": "V\u00e4lj Toon-sk\u00e4rm att ansluta till.", - "title": "V\u00e4lj sk\u00e4rm" - } + "no_agreements": "Det h\u00e4r kontot har inga Toon-sk\u00e4rmar." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/th.json b/homeassistant/components/toon/translations/th.json deleted file mode 100644 index 896d9ba8176..00000000000 --- a/homeassistant/components/toon/translations/th.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "authenticate": { - "data": { - "password": "\u0e23\u0e2b\u0e31\u0e2a\u0e1c\u0e48\u0e32\u0e19", - "username": "\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/zh-Hans.json b/homeassistant/components/toon/translations/zh-Hans.json deleted file mode 100644 index 94fe18d1656..00000000000 --- a/homeassistant/components/toon/translations/zh-Hans.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "authenticate": { - "data": { - "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } - } - } - } -} \ 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 e9aa599f079..a6d5227afc6 100644 --- a/homeassistant/components/toon/translations/zh-Hant.json +++ b/homeassistant/components/toon/translations/zh-Hant.json @@ -4,16 +4,8 @@ "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\u3002", - "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" - }, - "error": { - "credentials": "\u6240\u63d0\u4f9b\u7684\u6191\u8b49\u7121\u6548\u3002", - "display_exists": "\u6240\u9078\u64c7\u7684\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u8a2d\u5099\u3002" }, "step": { "agreement": { @@ -23,22 +15,6 @@ "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", - "tenant": "\u79df\u7528", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "description": "\u4f7f\u7528 Eneco Toon \u5e33\u865f\uff08\u975e\u958b\u767c\u8005\u5e33\u865f\uff09\u9032\u884c\u9a57\u8b49\u3002", - "title": "\u9023\u7d50 Toon \u5e33\u865f" - }, - "display": { - "data": { - "display": "\u9078\u64c7\u8a2d\u5099" - }, - "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/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index 7e319ca1cfd..96f3397af85 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/translations/ca.json @@ -12,9 +12,7 @@ "user": { "data": { "host": "Amfitri\u00f3", - "limit": "L\u00edmit", "name": "Nom", - "order": "Ordre", "password": "Contrasenya", "port": "Port", "username": "Nom d'usuari" diff --git a/homeassistant/components/transmission/translations/en.json b/homeassistant/components/transmission/translations/en.json index 42ff361555c..fe3758b774a 100644 --- a/homeassistant/components/transmission/translations/en.json +++ b/homeassistant/components/transmission/translations/en.json @@ -12,9 +12,7 @@ "user": { "data": { "host": "Host", - "limit": "Limit", "name": "Name", - "order": "Order", "password": "Password", "port": "Port", "username": "Username" diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index a760cb65ac7..a1c2de1af7e 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -12,9 +12,7 @@ "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/fr.json b/homeassistant/components/transmission/translations/fr.json index 83ff026e6e0..6bad4b15a81 100644 --- a/homeassistant/components/transmission/translations/fr.json +++ b/homeassistant/components/transmission/translations/fr.json @@ -12,9 +12,7 @@ "user": { "data": { "host": "H\u00f4te", - "limit": "Limite", "name": "Nom", - "order": "Ordre", "password": "Mot de passe", "port": "Port", "username": "Nom d'utilisateur" diff --git a/homeassistant/components/transmission/translations/it.json b/homeassistant/components/transmission/translations/it.json index 13a930bdfc7..0a9fa5a874e 100644 --- a/homeassistant/components/transmission/translations/it.json +++ b/homeassistant/components/transmission/translations/it.json @@ -12,9 +12,7 @@ "user": { "data": { "host": "Host", - "limit": "Limite", "name": "Nome", - "order": "Ordine", "password": "Password", "port": "Porta", "username": "Nome utente" diff --git a/homeassistant/components/transmission/translations/ko.json b/homeassistant/components/transmission/translations/ko.json index 4c1b968ddc4..5a041d8a54b 100644 --- a/homeassistant/components/transmission/translations/ko.json +++ b/homeassistant/components/transmission/translations/ko.json @@ -12,9 +12,7 @@ "user": { "data": { "host": "\ud638\uc2a4\ud2b8", - "limit": "\uc81c\ud55c", "name": "\uc774\ub984", - "order": "\uc21c\uc11c", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" diff --git a/homeassistant/components/transmission/translations/lb.json b/homeassistant/components/transmission/translations/lb.json index eb305ea7980..1da78c9ce0c 100644 --- a/homeassistant/components/transmission/translations/lb.json +++ b/homeassistant/components/transmission/translations/lb.json @@ -12,9 +12,7 @@ "user": { "data": { "host": "Host", - "limit": "Limite", "name": "Numm", - "order": "Reiefolleg", "password": "Passwuert", "port": "Port", "username": "Benotzernumm" diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index 89150467222..48b86516917 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -12,9 +12,7 @@ "user": { "data": { "host": "Vert", - "limit": "Grense", "name": "Navn", - "order": "Rekkef\u00f8lge", "password": "Passord", "port": "", "username": "Brukernavn" diff --git a/homeassistant/components/transmission/translations/pl.json b/homeassistant/components/transmission/translations/pl.json index 56b569a8c10..4f62dcfb882 100644 --- a/homeassistant/components/transmission/translations/pl.json +++ b/homeassistant/components/transmission/translations/pl.json @@ -12,9 +12,7 @@ "user": { "data": { "host": "Nazwa hosta lub adres IP", - "limit": "Limit", "name": "Nazwa", - "order": "Kolejno\u015b\u0107", "password": "Has\u0142o", "port": "Port", "username": "Nazwa u\u017cytkownika" diff --git a/homeassistant/components/transmission/translations/pt.json b/homeassistant/components/transmission/translations/pt.json index 380fa6cc5d7..70b7789adee 100644 --- a/homeassistant/components/transmission/translations/pt.json +++ b/homeassistant/components/transmission/translations/pt.json @@ -7,9 +7,7 @@ "user": { "data": { "host": "Servidor", - "limit": "Limite", "name": "Nome", - "order": "Ordem", "password": "Palavra-passe", "port": "Porta", "username": "Nome de Utilizador" diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index c51d32733a8..b6bbe58ea1a 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -12,9 +12,7 @@ "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" diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index 3d343ef2e3e..0dcbd97d183 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -12,9 +12,7 @@ "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" From 970c0e7594285e6403f105b919a587f3f18fab9c Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 5 Aug 2020 21:02:28 -0700 Subject: [PATCH 003/862] Bump androidtv to 0.0.48 and pure-python-adb to 0.3.0.dev0 (#38578) --- homeassistant/components/androidtv/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 7a76a618756..5b612c3f4c7 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,8 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.2.1", - "androidtv[async]==0.0.47", - "pure-python-adb==0.2.2.dev0" + "androidtv[async]==0.0.48", + "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"] } diff --git a/requirements_all.txt b/requirements_all.txt index c144f8207fc..d54cbea62eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -240,7 +240,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.47 +androidtv[async]==0.0.48 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -1138,7 +1138,7 @@ pubnubsub-handler==1.0.8 pulsectl==20.2.4 # homeassistant.components.androidtv -pure-python-adb==0.2.2.dev0 +pure-python-adb[async]==0.3.0.dev0 # homeassistant.components.pushbullet pushbullet.py==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 722fbe0bcc5..3bf4f6ee196 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -141,7 +141,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.47 +androidtv[async]==0.0.48 # homeassistant.components.apns apns2==0.3.0 @@ -534,7 +534,7 @@ prometheus_client==0.7.1 ptvsd==4.3.2 # homeassistant.components.androidtv -pure-python-adb==0.2.2.dev0 +pure-python-adb[async]==0.3.0.dev0 # homeassistant.components.pushbullet pushbullet.py==0.11.0 From 9d9426f24cf6868aae68f4bd030b0b68cdc7bfe7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 6 Aug 2020 08:32:53 +0200 Subject: [PATCH 004/862] Fix missing rfxtrx strings (#38570) * Fix missing rfxtrx strings * Clean --- homeassistant/components/rfxtrx/strings.json | 9 ++++++++- homeassistant/components/rfxtrx/translations/en.json | 9 +++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/rfxtrx/translations/en.json diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 7a73a41bfdf..e19265dec32 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -1,2 +1,9 @@ { -} \ No newline at end of file + "config": { + "step": {}, + "error": {}, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json new file mode 100644 index 00000000000..263b2a9467b --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": {}, + "step": {} + } +} From 896bdbff8f5bb337b5a4b678de0029c5aa7542f7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 6 Aug 2020 09:32:42 +0200 Subject: [PATCH 005/862] Revert "Add a timeout for async_add_entities (#38474)" (#38584) This reverts commit 7590af393077fbee840a7e91921717a4b699a553. --- homeassistant/helpers/entity_platform.py | 28 ++---------------- tests/helpers/test_entity_platform.py | 36 ------------------------ 2 files changed, 3 insertions(+), 61 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 5f6d2349ec7..7a581dbd19e 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,6 +1,5 @@ """Class to manage the entities for a single platform.""" import asyncio -from contextlib import suppress from contextvars import ContextVar from datetime import datetime, timedelta from logging import Logger @@ -24,8 +23,6 @@ if TYPE_CHECKING: SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 -SLOW_ADD_ENTITIES_MAX_WAIT = 60 - PLATFORM_NOT_READY_RETRIES = 10 DATA_ENTITY_PLATFORM = "entity_platform" PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds @@ -285,10 +282,8 @@ class EntityPlatform: device_registry = await hass.helpers.device_registry.async_get_registry() entity_registry = await hass.helpers.entity_registry.async_get_registry() tasks = [ - asyncio.create_task( - self._async_add_entity( # type: ignore - entity, update_before_add, entity_registry, device_registry - ) + self._async_add_entity( # type: ignore + entity, update_before_add, entity_registry, device_registry ) for entity in new_entities ] @@ -297,24 +292,7 @@ class EntityPlatform: if not tasks: return - await asyncio.wait(tasks, timeout=SLOW_ADD_ENTITIES_MAX_WAIT) - - for idx, entity in enumerate(new_entities): - task = tasks[idx] - if task.done(): - await task - continue - - self.logger.warning( - "Timed out adding entity %s for domain %s with platform %s after %ds.", - entity.entity_id, - self.domain, - self.platform_name, - SLOW_ADD_ENTITIES_MAX_WAIT, - ) - task.cancel() - with suppress(asyncio.CancelledError): - await task + await asyncio.gather(*tasks) if self._async_unsub_polling is not None or not any( entity.should_poll for entity in self.entities.values() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 3de68dca4c2..5912eb42b03 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -931,39 +931,3 @@ async def test_invalid_entity_id(hass): await platform.async_add_entities([entity]) assert entity.hass is None assert entity.platform is None - - -class MockBlockingEntity(MockEntity): - """Class to mock an entity that will block adding entities.""" - - async def async_added_to_hass(self): - """Block for a long time.""" - await asyncio.sleep(1000) - - -async def test_setup_entry_with_entities_that_block_forever(hass, caplog): - """Test we cancel adding entities when we reach the timeout.""" - registry = mock_registry(hass) - - async def async_setup_entry(hass, config_entry, async_add_entities): - """Mock setup entry method.""" - async_add_entities([MockBlockingEntity(name="test1", unique_id="unique")]) - return True - - platform = MockPlatform(async_setup_entry=async_setup_entry) - config_entry = MockConfigEntry(entry_id="super-mock-id") - mock_entity_platform = MockEntityPlatform( - hass, platform_name=config_entry.domain, platform=platform - ) - - with patch.object(entity_platform, "SLOW_ADD_ENTITIES_MAX_WAIT", 0.01): - assert await mock_entity_platform.async_setup_entry(config_entry) - await hass.async_block_till_done() - full_name = f"{mock_entity_platform.domain}.{config_entry.domain}" - assert full_name in hass.config.components - assert len(hass.states.async_entity_ids()) == 0 - assert len(registry.entities) == 1 - assert "Timed out adding entity" in caplog.text - assert "test_domain.test1" in caplog.text - assert "test_domain" in caplog.text - assert "test" in caplog.text From 4ed1f8023b3e369d3867f0121e8a82b8e3d55c1b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 6 Aug 2020 10:43:47 +0200 Subject: [PATCH 006/862] Suppress MQTT discovery updates without changes (#38568) --- homeassistant/components/mqtt/__init__.py | 12 +++++++--- .../components/mqtt/binary_sensor.py | 3 ++- .../components/mqtt/light/schema_template.py | 4 ++-- .../mqtt/test_alarm_control_panel.py | 16 +++++++++++++ tests/components/mqtt/test_binary_sensor.py | 15 ++++++++++++ tests/components/mqtt/test_camera.py | 19 ++++++++++++--- tests/components/mqtt/test_climate.py | 18 ++++++++++++--- tests/components/mqtt/test_common.py | 23 +++++++++++++++++++ tests/components/mqtt/test_cover.py | 23 +++++++++++++++---- tests/components/mqtt/test_fan.py | 21 +++++++++++++---- tests/components/mqtt/test_legacy_vacuum.py | 13 +++++++++++ tests/components/mqtt/test_light.py | 16 +++++++++++++ tests/components/mqtt/test_light_json.py | 17 ++++++++++++++ tests/components/mqtt/test_light_template.py | 19 +++++++++++++++ tests/components/mqtt/test_lock.py | 17 ++++++++++++++ tests/components/mqtt/test_sensor.py | 22 ++++++++++++++---- tests/components/mqtt/test_state_vacuum.py | 23 +++++++++++++++---- tests/components/mqtt/test_switch.py | 16 +++++++++++++ 18 files changed, 266 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a0527cfe427..81c44ac8aea 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -44,6 +44,7 @@ from . import config_flow # noqa: F401 pylint: disable=unused-import from . import debug_info, discovery from .const import ( ATTR_DISCOVERY_HASH, + ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC, ATTR_PAYLOAD, ATTR_QOS, @@ -1169,6 +1170,7 @@ class MqttDiscoveryUpdate(Entity): _LOGGER.info( "Got update for entity with hash: %s '%s'", discovery_hash, payload, ) + old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD] debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) if not payload: # Empty payload: Remove component @@ -1176,9 +1178,13 @@ class MqttDiscoveryUpdate(Entity): self._cleanup_discovery_on_remove() await _async_remove_state_and_registry_entry(self) elif self._discovery_update: - # Non-empty payload: Notify component - _LOGGER.info("Updating component: %s", self.entity_id) - await self._discovery_update(payload) + if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: + # Non-empty, changed payload: Notify component + _LOGGER.info("Updating component: %s", self.entity_id) + await self._discovery_update(payload) + else: + # Non-empty, unchanged payload: Ignore to avoid changing states + _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) if discovery_hash: debug_info.add_entity_discovery_data( diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index cd69967e6a7..5d69bfde4f6 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -169,7 +169,8 @@ class MqttBinarySensor( if expire_after is not None and expire_after > 0: - # When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + # When expire_after is set, and we receive a message, assume device is + # not expired since it has to be to receive the message self._expired = False # Reset old trigger diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index a9f18e7039b..d14cda70bb6 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -101,10 +101,10 @@ async def async_setup_entity_template( config, async_add_entities, config_entry, discovery_data ): """Set up a MQTT Template light.""" - async_add_entities([MqttTemplate(config, config_entry, discovery_data)]) + async_add_entities([MqttLightTemplate(config, config_entry, discovery_data)]) -class MqttTemplate( +class MqttLightTemplate( MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index aa6452fd9c8..734e1fd552f 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -28,6 +28,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -42,6 +43,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_mqtt_message from tests.components.alarm_control_panel import common @@ -575,6 +577,20 @@ async def test_discovery_update_alarm(hass, mqtt_mock, caplog): ) +async def test_discovery_update_unchanged_alarm(hass, mqtt_mock, caplog): + """Test update of discovered alarm_control_panel.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config1["name"] = "Beer" + + data1 = json.dumps(config1) + with patch( + "homeassistant.components.mqtt.alarm_control_panel.MqttAlarm.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index b909a0592e0..c739f4378d1 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -26,6 +26,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -593,6 +594,20 @@ async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog): ) +async def test_discovery_update_unchanged_binary_sensor(hass, mqtt_mock, caplog): + """Test update of discovered binary_sensor.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config1["name"] = "Beer" + + data1 = json.dumps(config1) + with patch( + "homeassistant.components.mqtt.binary_sensor.MqttBinarySensor.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, discovery_update + ) + + async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( hass, mqtt_mock, legacy_patchable_time, caplog ): diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 6869b530668..22f714fdcf7 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -16,6 +16,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -30,6 +31,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { @@ -153,14 +155,25 @@ async def test_discovery_update_camera(hass, mqtt_mock, caplog): entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] await async_start(hass, "homeassistant", entry) - data1 = '{ "name": "Beer",' ' "topic": "test_topic"}' - data2 = '{ "name": "Milk",' ' "topic": "test_topic"}' + data1 = '{ "name": "Beer", "topic": "test_topic"}' + data2 = '{ "name": "Milk", "topic": "test_topic"}' await help_test_discovery_update( hass, mqtt_mock, caplog, camera.DOMAIN, data1, data2 ) +async def test_discovery_update_unchanged_camera(hass, mqtt_mock, caplog): + """Test update of discovered camera.""" + data1 = '{ "name": "Beer", "topic": "test_topic"}' + with patch( + "homeassistant.components.mqtt.camera.MqttCamera.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, camera.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" @@ -168,7 +181,7 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): await async_start(hass, "homeassistant", entry) data1 = '{ "name": "Beer" }' - data2 = '{ "name": "Milk",' ' "topic": "test_topic"}' + data2 = '{ "name": "Milk", "topic": "test_topic"}' await help_test_discovery_broken( hass, mqtt_mock, caplog, camera.DOMAIN, data1, data2 diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 6a7bdf0b7e6..d60af211d71 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -34,6 +34,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -48,7 +49,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import call +from tests.async_mock import call, patch from tests.common import async_fire_mqtt_message from tests.components.climate import common @@ -909,11 +910,22 @@ async def test_discovery_update_climate(hass, mqtt_mock, caplog): ) +async def test_discovery_update_unchanged_climate(hass, mqtt_mock, caplog): + """Test update of discovered climate.""" + data1 = '{ "name": "Beer" }' + with patch( + "homeassistant.components.mqtt.climate.MqttClimate.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" - data1 = '{ "name": "Beer",' ' "power_command_topic": "test_topic#" }' - data2 = '{ "name": "Milk", ' ' "power_command_topic": "test_topic" }' + data1 = '{ "name": "Beer", "power_command_topic": "test_topic#" }' + data2 = '{ "name": "Milk", "power_command_topic": "test_topic" }' await help_test_discovery_broken( hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 31566885a37..89bfde22d87 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -497,6 +497,29 @@ async def help_test_discovery_update(hass, mqtt_mock, caplog, domain, data1, dat assert state is None +async def help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, domain, data1, discovery_update +): + """Test update of discovered component without changes. + + This is a test helper for the MqttDiscoveryUpdate mixin. + """ + entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + await async_start(hass, "homeassistant", entry) + + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) + await hass.async_block_till_done() + + state = hass.states.get(f"{domain}.beer") + assert state is not None + assert state.name == "Beer" + + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) + await hass.async_block_till_done() + + assert not discovery_update.called + + async def help_test_discovery_broken(hass, mqtt_mock, caplog, domain, data1, data2): """Test handling of bad discovery message.""" entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index c3f00badef8..f9036bcfa0f 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -38,6 +38,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -52,6 +53,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { @@ -1862,24 +1864,35 @@ async def test_unique_id(hass, mqtt_mock): async def test_discovery_removal_cover(hass, mqtt_mock, caplog): """Test removal of discovered cover.""" - data = '{ "name": "test",' ' "command_topic": "test_topic" }' + data = '{ "name": "test", "command_topic": "test_topic" }' await help_test_discovery_removal(hass, mqtt_mock, caplog, cover.DOMAIN, data) async def test_discovery_update_cover(hass, mqtt_mock, caplog): """Test update of discovered cover.""" - data1 = '{ "name": "Beer",' ' "command_topic": "test_topic" }' - data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' + data1 = '{ "name": "Beer", "command_topic": "test_topic" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_update( hass, mqtt_mock, caplog, cover.DOMAIN, data1, data2 ) +async def test_discovery_update_unchanged_cover(hass, mqtt_mock, caplog): + """Test update of discovered cover.""" + data1 = '{ "name": "Beer", "command_topic": "test_topic" }' + with patch( + "homeassistant.components.mqtt.cover.MqttCover.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, cover.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" - data1 = '{ "name": "Beer",' ' "command_topic": "test_topic#" }' - data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' + data1 = '{ "name": "Beer", "command_topic": "test_topic#" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_broken( hass, mqtt_mock, caplog, cover.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 6114fe48ff4..e1801c5c15a 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -19,6 +19,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -33,6 +34,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.fan import common @@ -689,22 +691,33 @@ async def test_unique_id(hass, mqtt_mock): async def test_discovery_removal_fan(hass, mqtt_mock, caplog): """Test removal of discovered fan.""" - data = '{ "name": "test",' ' "command_topic": "test_topic" }' + data = '{ "name": "test", "command_topic": "test_topic" }' await help_test_discovery_removal(hass, mqtt_mock, caplog, fan.DOMAIN, data) async def test_discovery_update_fan(hass, mqtt_mock, caplog): """Test update of discovered fan.""" - data1 = '{ "name": "Beer",' ' "command_topic": "test_topic" }' - data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' + data1 = '{ "name": "Beer", "command_topic": "test_topic" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_update(hass, mqtt_mock, caplog, fan.DOMAIN, data1, data2) +async def test_discovery_update_unchanged_fan(hass, mqtt_mock, caplog): + """Test update of discovered fan.""" + data1 = '{ "name": "Beer", "command_topic": "test_topic" }' + with patch( + "homeassistant.components.mqtt.fan.MqttFan.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, fan.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' - data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' + data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_broken(hass, mqtt_mock, caplog, fan.DOMAIN, data1, data2) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 893c1b78f1e..aacea4e345e 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -31,6 +31,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -45,6 +46,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.vacuum import common @@ -643,6 +645,17 @@ async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): ) +async def test_discovery_update_unchanged_vacuum(hass, mqtt_mock, caplog): + """Test update of discovered vacuum.""" + data1 = '{ "name": "Beer", "command_topic": "test_topic" }' + with patch( + "homeassistant.components.mqtt.vacuum.schema_legacy.MqttVacuum.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 5fa8fa181e5..75d3e694838 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -170,6 +170,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -1450,6 +1451,21 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): ) +async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): + """Test update of discovered light.""" + data1 = ( + '{ "name": "Beer",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + with patch( + "homeassistant.components.mqtt.light.schema_basic.MqttLight.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 7bb3763654e..54292aeeb7b 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -110,6 +110,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -1179,6 +1180,22 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): ) +async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): + """Test update of discovered light.""" + data1 = ( + '{ "name": "Beer",' + ' "schema": "json",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + with patch( + "homeassistant.components.mqtt.light.schema_json.MqttLightJson.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index f0e226d2095..17b3332da40 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -47,6 +47,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -923,6 +924,24 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): ) +async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): + """Test update of discovered light.""" + data1 = ( + '{ "name": "Beer",' + ' "schema": "template",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic",' + ' "command_on_template": "on",' + ' "command_off_template": "off"}' + ) + with patch( + "homeassistant.components.mqtt.light.schema_template.MqttLightTemplate.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index ff130077a95..cd37543d94e 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -20,6 +20,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -34,6 +35,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { @@ -382,6 +384,21 @@ async def test_discovery_update_lock(hass, mqtt_mock, caplog): await help_test_discovery_update(hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, data2) +async def test_discovery_update_unchanged_lock(hass, mqtt_mock, caplog): + """Test update of discovered lock.""" + data1 = ( + '{ "name": "Beer",' + ' "state_topic": "test_topic",' + ' "command_topic": "command_topic" }' + ) + with patch( + "homeassistant.components.mqtt.lock.MqttLock.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 5ec5fccbe28..0d31b9f33f2 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -24,6 +24,7 @@ from .test_common import ( help_test_discovery_update, help_test_discovery_update_attr, help_test_discovery_update_availability, + help_test_discovery_update_unchanged, help_test_entity_debug_info, help_test_entity_debug_info_max_messages, help_test_entity_debug_info_message, @@ -425,24 +426,35 @@ async def test_unique_id(hass, mqtt_mock): async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): """Test removal of discovered sensor.""" - data = '{ "name": "test",' ' "state_topic": "test_topic" }' + data = '{ "name": "test", "state_topic": "test_topic" }' await help_test_discovery_removal(hass, mqtt_mock, caplog, sensor.DOMAIN, data) async def test_discovery_update_sensor(hass, mqtt_mock, caplog): """Test update of discovered sensor.""" - data1 = '{ "name": "Beer",' ' "state_topic": "test_topic" }' - data2 = '{ "name": "Milk",' ' "state_topic": "test_topic" }' + data1 = '{ "name": "Beer", "state_topic": "test_topic" }' + data2 = '{ "name": "Milk", "state_topic": "test_topic" }' await help_test_discovery_update( hass, mqtt_mock, caplog, sensor.DOMAIN, data1, data2 ) +async def test_discovery_update_unchanged_sensor(hass, mqtt_mock, caplog): + """Test update of discovered sensor.""" + data1 = '{ "name": "Beer", "state_topic": "test_topic" }' + with patch( + "homeassistant.components.mqtt.sensor.MqttSensor.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, sensor.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" - data1 = '{ "name": "Beer",' ' "state_topic": "test_topic#" }' - data2 = '{ "name": "Milk",' ' "state_topic": "test_topic" }' + data1 = '{ "name": "Beer", "state_topic": "test_topic#" }' + data2 = '{ "name": "Milk", "state_topic": "test_topic" }' await help_test_discovery_broken( hass, mqtt_mock, caplog, sensor.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index c8ca7d3691b..fe410821395 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -41,6 +41,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -55,6 +56,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.vacuum import common @@ -410,24 +412,35 @@ async def test_unique_id(hass, mqtt_mock): async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): """Test removal of discovered vacuum.""" - data = '{ "schema": "state", "name": "test",' ' "command_topic": "test_topic"}' + data = '{ "schema": "state", "name": "test", "command_topic": "test_topic"}' await help_test_discovery_removal(hass, mqtt_mock, caplog, vacuum.DOMAIN, data) async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): """Test update of discovered vacuum.""" - data1 = '{ "schema": "state", "name": "Beer",' ' "command_topic": "test_topic"}' - data2 = '{ "schema": "state", "name": "Milk",' ' "command_topic": "test_topic"}' + data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}' + data2 = '{ "schema": "state", "name": "Milk", "command_topic": "test_topic"}' await help_test_discovery_update( hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 ) +async def test_discovery_update_unchanged_vacuum(hass, mqtt_mock, caplog): + """Test update of discovered vacuum.""" + data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}' + with patch( + "homeassistant.components.mqtt.vacuum.schema_state.MqttStateVacuum.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" - data1 = '{ "schema": "state", "name": "Beer",' ' "command_topic": "test_topic#"}' - data2 = '{ "schema": "state", "name": "Milk",' ' "command_topic": "test_topic"}' + data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic#"}' + data2 = '{ "schema": "state", "name": "Milk", "command_topic": "test_topic"}' await help_test_discovery_broken( hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 869a413eb6b..a6edb8d6f14 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -15,6 +15,7 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_discovery_update_unchanged, help_test_entity_debug_info_message, help_test_entity_device_info_remove, help_test_entity_device_info_update, @@ -320,6 +321,21 @@ async def test_discovery_update_switch(hass, mqtt_mock, caplog): ) +async def test_discovery_update_unchanged_switch(hass, mqtt_mock, caplog): + """Test update of discovered switch.""" + data1 = ( + '{ "name": "Beer",' + ' "state_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + with patch( + "homeassistant.components.mqtt.switch.MqttSwitch.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, switch.DOMAIN, data1, discovery_update + ) + + @pytest.mark.no_fail_on_log_exception async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" From 21f4d694bbef7e18ff98b030974882f6982464dc Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 6 Aug 2020 11:18:05 +0200 Subject: [PATCH 007/862] Remove Linky integration (#38565) --- .coveragerc | 2 - CODEOWNERS | 1 - homeassistant/components/linky/__init__.py | 64 ------ homeassistant/components/linky/config_flow.py | 99 ---------- homeassistant/components/linky/const.py | 5 - homeassistant/components/linky/manifest.json | 8 - homeassistant/components/linky/sensor.py | 162 ---------------- homeassistant/components/linky/strings.json | 23 --- .../components/linky/translations/bg.json | 20 -- .../components/linky/translations/ca.json | 23 --- .../components/linky/translations/cs.json | 15 -- .../components/linky/translations/da.json | 23 --- .../components/linky/translations/de.json | 23 --- .../components/linky/translations/en.json | 23 --- .../components/linky/translations/es-419.json | 23 --- .../components/linky/translations/es.json | 23 --- .../components/linky/translations/fr.json | 23 --- .../components/linky/translations/hu.json | 21 -- .../components/linky/translations/it.json | 23 --- .../components/linky/translations/ko.json | 23 --- .../components/linky/translations/lb.json | 23 --- .../components/linky/translations/lv.json | 11 -- .../components/linky/translations/nl.json | 23 --- .../components/linky/translations/nn.json | 9 - .../components/linky/translations/no.json | 23 --- .../components/linky/translations/pl.json | 23 --- .../components/linky/translations/pt-BR.json | 17 -- .../components/linky/translations/pt.json | 12 -- .../components/linky/translations/ru.json | 23 --- .../components/linky/translations/sl.json | 23 --- .../components/linky/translations/sv.json | 23 --- .../linky/translations/zh-Hans.json | 16 -- .../linky/translations/zh-Hant.json | 23 --- homeassistant/generated/config_flows.py | 1 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/linky/__init__.py | 1 - tests/components/linky/conftest.py | 11 -- tests/components/linky/test_config_flow.py | 182 ------------------ 39 files changed, 1077 deletions(-) delete mode 100644 homeassistant/components/linky/__init__.py delete mode 100644 homeassistant/components/linky/config_flow.py delete mode 100644 homeassistant/components/linky/const.py delete mode 100644 homeassistant/components/linky/manifest.json delete mode 100644 homeassistant/components/linky/sensor.py delete mode 100644 homeassistant/components/linky/strings.json delete mode 100644 homeassistant/components/linky/translations/bg.json delete mode 100644 homeassistant/components/linky/translations/ca.json delete mode 100644 homeassistant/components/linky/translations/cs.json delete mode 100644 homeassistant/components/linky/translations/da.json delete mode 100644 homeassistant/components/linky/translations/de.json delete mode 100644 homeassistant/components/linky/translations/en.json delete mode 100644 homeassistant/components/linky/translations/es-419.json delete mode 100644 homeassistant/components/linky/translations/es.json delete mode 100644 homeassistant/components/linky/translations/fr.json delete mode 100644 homeassistant/components/linky/translations/hu.json delete mode 100644 homeassistant/components/linky/translations/it.json delete mode 100644 homeassistant/components/linky/translations/ko.json delete mode 100644 homeassistant/components/linky/translations/lb.json delete mode 100644 homeassistant/components/linky/translations/lv.json delete mode 100644 homeassistant/components/linky/translations/nl.json delete mode 100644 homeassistant/components/linky/translations/nn.json delete mode 100644 homeassistant/components/linky/translations/no.json delete mode 100644 homeassistant/components/linky/translations/pl.json delete mode 100644 homeassistant/components/linky/translations/pt-BR.json delete mode 100644 homeassistant/components/linky/translations/pt.json delete mode 100644 homeassistant/components/linky/translations/ru.json delete mode 100644 homeassistant/components/linky/translations/sl.json delete mode 100644 homeassistant/components/linky/translations/sv.json delete mode 100644 homeassistant/components/linky/translations/zh-Hans.json delete mode 100644 homeassistant/components/linky/translations/zh-Hant.json delete mode 100644 tests/components/linky/__init__.py delete mode 100644 tests/components/linky/conftest.py delete mode 100644 tests/components/linky/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 6f286fc6a69..f340202cdb8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -460,8 +460,6 @@ omit = homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py homeassistant/components/linksys_smart/device_tracker.py - homeassistant/components/linky/__init__.py - homeassistant/components/linky/sensor.py homeassistant/components/linode/* homeassistant/components/linux_battery/sensor.py homeassistant/components/lirc/* diff --git a/CODEOWNERS b/CODEOWNERS index 3c4ac8dd0dd..0081057f086 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -226,7 +226,6 @@ homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus homeassistant/components/life360/* @pnbruckner -homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py deleted file mode 100644 index d21c007762c..00000000000 --- a/homeassistant/components/linky/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -"""The linky component.""" -import logging - -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType - -from .const import DEFAULT_TIMEOUT, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -ACCOUNT_SCHEMA = vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } -) - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))}, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up Linky sensors from legacy config file.""" - - conf = config.get(DOMAIN) - if conf is None: - return True - - for linky_account_conf in conf: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=linky_account_conf.copy(), - ) - ) - - return True - - -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): - """Set up Linky sensors.""" - # For backwards compat - if entry.unique_id is None: - hass.config_entries.async_update_entry( - entry, unique_id=entry.data[CONF_USERNAME] - ) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - return True - - -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): - """Unload Linky sensors.""" - return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/linky/config_flow.py b/homeassistant/components/linky/config_flow.py deleted file mode 100644 index 88fa725cc4a..00000000000 --- a/homeassistant/components/linky/config_flow.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Config flow to configure the Linky integration.""" -import logging - -from pylinky.client import LinkyClient -from pylinky.exceptions import ( - PyLinkyAccessException, - PyLinkyEnedisException, - PyLinkyException, - PyLinkyWrongLoginException, -) -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME - -from .const import DEFAULT_TIMEOUT -from .const import DOMAIN # pylint: disable=unused-import - -_LOGGER = logging.getLogger(__name__) - - -class LinkyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow.""" - - VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - - def _show_setup_form(self, user_input=None, errors=None): - """Show the setup form to the user.""" - - if user_input is None: - user_input = {} - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required( - CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") - ): str, - vol.Required( - CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") - ): str, - } - ), - errors=errors or {}, - ) - - async def async_step_user(self, user_input=None): - """Handle a flow initiated by the user.""" - errors = {} - - if user_input is None: - return self._show_setup_form(user_input, None) - - username = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] - timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) - - # Check if already configured - if self.unique_id is None: - await self.async_set_unique_id(username) - self._abort_if_unique_id_configured() - - client = LinkyClient(username, password, None, timeout) - try: - await self.hass.async_add_executor_job(client.login) - await self.hass.async_add_executor_job(client.fetch_data) - except PyLinkyAccessException as exp: - _LOGGER.error(exp) - errors["base"] = "access" - return self._show_setup_form(user_input, errors) - except PyLinkyEnedisException as exp: - _LOGGER.error(exp) - errors["base"] = "enedis" - return self._show_setup_form(user_input, errors) - except PyLinkyWrongLoginException as exp: - _LOGGER.error(exp) - errors["base"] = "wrong_login" - return self._show_setup_form(user_input, errors) - except PyLinkyException as exp: - _LOGGER.error(exp) - errors["base"] = "unknown" - return self._show_setup_form(user_input, errors) - finally: - client.close_session() - - return self.async_create_entry( - title=username, - data={ - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_TIMEOUT: timeout, - }, - ) - - async def async_step_import(self, user_input=None): - """Import a config entry.""" - return await self.async_step_user(user_input) diff --git a/homeassistant/components/linky/const.py b/homeassistant/components/linky/const.py deleted file mode 100644 index e8e68867528..00000000000 --- a/homeassistant/components/linky/const.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Linky component constants.""" - -DOMAIN = "linky" - -DEFAULT_TIMEOUT = 10 diff --git a/homeassistant/components/linky/manifest.json b/homeassistant/components/linky/manifest.json deleted file mode 100644 index 18ee74a78ce..00000000000 --- a/homeassistant/components/linky/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "linky", - "name": "Enedis Linky", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/linky", - "requirements": ["pylinky==0.4.0"], - "codeowners": ["@Quentame"] -} diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py deleted file mode 100644 index 7e9da01eb9a..00000000000 --- a/homeassistant/components/linky/sensor.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Support for Linky.""" -from datetime import timedelta -import json -import logging - -from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient, PyLinkyException - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_PASSWORD, - CONF_TIMEOUT, - CONF_USERNAME, - ENERGY_KILO_WATT_HOUR, -) -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(hours=4) -ICON_ENERGY = "mdi:flash" -CONSUMPTION = "conso" -TIME = "time" -INDEX_CURRENT = -1 -INDEX_LAST = -2 -ATTRIBUTION = "Data provided by Enedis" - - -async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities -) -> None: - """Add Linky entries.""" - account = LinkyAccount( - entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.data[CONF_TIMEOUT] - ) - - await hass.async_add_executor_job(account.update_linky_data) - - sensors = [ - LinkySensor("Linky yesterday", account, DAILY, INDEX_LAST), - LinkySensor("Linky current month", account, MONTHLY, INDEX_CURRENT), - LinkySensor("Linky last month", account, MONTHLY, INDEX_LAST), - LinkySensor("Linky current year", account, YEARLY, INDEX_CURRENT), - LinkySensor("Linky last year", account, YEARLY, INDEX_LAST), - ] - - async_track_time_interval(hass, account.update_linky_data, SCAN_INTERVAL) - - async_add_entities(sensors, True) - - -class LinkyAccount: - """Representation of a Linky account.""" - - def __init__(self, username, password, timeout): - """Initialise the Linky account.""" - self._username = username - self._password = password - self._timeout = timeout - self._data = None - - def update_linky_data(self, event_time=None): - """Fetch new state data for the sensor.""" - client = LinkyClient(self._username, self._password, None, self._timeout) - try: - client.login() - client.fetch_data() - self._data = client.get_data() - _LOGGER.debug(json.dumps(self._data, indent=2)) - except PyLinkyException as exp: - _LOGGER.error(exp) - raise PlatformNotReady - finally: - client.close_session() - - @property - def username(self): - """Return the username.""" - return self._username - - @property - def data(self): - """Return the data.""" - return self._data - - -class LinkySensor(Entity): - """Representation of a sensor entity for Linky.""" - - def __init__(self, name, account: LinkyAccount, scale, when): - """Initialize the sensor.""" - self._name = name - self._account = account - self._scale = scale - self._when = when - self._username = account.username - self._time = None - self._consumption = None - self._unique_id = f"{self._username}_{scale}_{when}" - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._consumption - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return ENERGY_KILO_WATT_HOUR - - @property - def icon(self): - """Return the icon of the sensor.""" - return ICON_ENERGY - - @property - def device_state_attributes(self): - """Return the state attributes of the sensor.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - "time": self._time, - CONF_USERNAME: self._username, - } - - @property - def device_info(self): - """Return device information.""" - return { - "identifiers": {(DOMAIN, self._username)}, - "name": "Linky meter", - "manufacturer": "Enedis", - } - - async def async_update(self) -> None: - """Retrieve the new data for the sensor.""" - if self._account.data is None: - return - - data = self._account.data[self._scale][self._when] - self._consumption = data[CONSUMPTION] - self._time = data[TIME] - - if self._scale is not YEARLY: - year_index = INDEX_CURRENT - if self._time.endswith("Dec"): - year_index = INDEX_LAST - self._time += f" {self._account.data[YEARLY][year_index][TIME]}" diff --git a/homeassistant/components/linky/strings.json b/homeassistant/components/linky/strings.json deleted file mode 100644 index dea7062d213..00000000000 --- a/homeassistant/components/linky/strings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Linky", - "description": "Enter your credentials", - "data": { - "username": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]" - } - } - }, - "error": { - "access": "Could not access to Enedis.fr, please check your internet connection", - "enedis": "Enedis.fr answered with an error: please retry later (usually not between 11PM and 2AM)", - "wrong_login": "Login error: please check your email & password", - "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)" - }, - "abort": { - "already_configured": "Account already configured" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/bg.json b/homeassistant/components/linky/translations/bg.json deleted file mode 100644 index dd337013f59..00000000000 --- a/homeassistant/components/linky/translations/bg.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "error": { - "access": "\u041d\u044f\u043c\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e Enedis.fr, \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0438", - "enedis": "Enedis.fr \u043e\u0442\u0433\u043e\u0432\u043e\u0440\u0438 \u0441 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", - "wrong_login": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435: \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0438\u043c\u0435\u0439\u043b\u0430 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438" - }, - "step": { - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "username": "E-mail" - }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0434\u0435\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u0441\u0438 \u0434\u0430\u043d\u043d\u0438", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/ca.json b/homeassistant/components/linky/translations/ca.json deleted file mode 100644 index 954b873083a..00000000000 --- a/homeassistant/components/linky/translations/ca.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "El compte ja ha estat configurat" - }, - "error": { - "access": "No s'ha pogut accedir a Enedis.fr, comprova la teva connexi\u00f3 a Internet", - "enedis": "Enedis.fr ha respost amb un error: torna-ho a provar m\u00e9s tard (millo no entre les 23:00 i les 14:00)", - "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard (millor no entre les 23:00 i les 14:00)", - "wrong_login": "Error d'inici de sessi\u00f3: comprova el teu correu electr\u00f2nic i la contrasenya" - }, - "step": { - "user": { - "data": { - "password": "Contrasenya", - "username": "Correu electr\u00f2nic" - }, - "description": "Introdueix les teves credencials", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/cs.json b/homeassistant/components/linky/translations/cs.json deleted file mode 100644 index 8f8c4648d5f..00000000000 --- a/homeassistant/components/linky/translations/cs.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "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/linky/translations/da.json b/homeassistant/components/linky/translations/da.json deleted file mode 100644 index 2fa885d1ffa..00000000000 --- a/homeassistant/components/linky/translations/da.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Kontoen er allerede konfigureret" - }, - "error": { - "access": "Kunne ikke f\u00e5 adgang til Enedis.fr, kontroller din internetforbindelse", - "enedis": "Enedis.fr svarede med en fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)", - "unknown": "Ukendt fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)", - "wrong_login": "Loginfejl: Kontroller din e-mail og adgangskode" - }, - "step": { - "user": { - "data": { - "password": "Adgangskode", - "username": "E-mail" - }, - "description": "Indtast dine legitimationsoplysninger", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/de.json b/homeassistant/components/linky/translations/de.json deleted file mode 100644 index c915ddf0881..00000000000 --- a/homeassistant/components/linky/translations/de.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Konto bereits konfiguriert" - }, - "error": { - "access": "Konnte nicht auf Enedis.fr zugreifen, \u00fcberpr\u00fcfe bitte die Internetverbindung", - "enedis": "Enedis.fr antwortete mit einem Fehler: wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)", - "unknown": "Unbekannter Fehler: Wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)", - "wrong_login": "Login-Fehler: Pr\u00fcfe bitte E-Mail & Passwort" - }, - "step": { - "user": { - "data": { - "password": "Passwort", - "username": "E-Mail-Adresse" - }, - "description": "Gib deine Zugangsdaten ein", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/en.json b/homeassistant/components/linky/translations/en.json deleted file mode 100644 index 512c0567444..00000000000 --- a/homeassistant/components/linky/translations/en.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Account already configured" - }, - "error": { - "access": "Could not access to Enedis.fr, please check your internet connection", - "enedis": "Enedis.fr answered with an error: please retry later (usually not between 11PM and 2AM)", - "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)", - "wrong_login": "Login error: please check your email & password" - }, - "step": { - "user": { - "data": { - "password": "Password", - "username": "Email" - }, - "description": "Enter your credentials", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/es-419.json b/homeassistant/components/linky/translations/es-419.json deleted file mode 100644 index 58e44695fc8..00000000000 --- a/homeassistant/components/linky/translations/es-419.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La cuenta ya ha sido configurada" - }, - "error": { - "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet.", - "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", - "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", - "wrong_login": "Error de inicio de sesi\u00f3n: por favor revise su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" - }, - "step": { - "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Correo electr\u00f3nico" - }, - "description": "Ingrese sus credenciales", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/es.json b/homeassistant/components/linky/translations/es.json deleted file mode 100644 index ef07dc2ca75..00000000000 --- a/homeassistant/components/linky/translations/es.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" - }, - "error": { - "access": "No se pudo acceder a Enedis.fr, comprueba tu conexi\u00f3n a Internet", - "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11:00 y las 2 de la ma\u00f1ana)", - "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 23:00 y las 02:00 horas).", - "wrong_login": "Error de inicio de sesi\u00f3n: comprueba tu direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" - }, - "step": { - "user": { - "data": { - "password": "Contrase\u00f1a", - "username": "Correo electr\u00f3nico" - }, - "description": "Introduzca sus credenciales", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/fr.json b/homeassistant/components/linky/translations/fr.json deleted file mode 100644 index 71dba36dbe8..00000000000 --- a/homeassistant/components/linky/translations/fr.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9" - }, - "error": { - "access": "Impossible d'acc\u00e9der \u00e0 Enedis.fr, merci de v\u00e9rifier votre connexion internet", - "enedis": "Erreur d'Enedis.fr: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", - "unknown": "Erreur inconnue: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", - "wrong_login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe" - }, - "step": { - "user": { - "data": { - "password": "Mot de passe", - "username": "Email" - }, - "description": "Entrez vos identifiants", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/hu.json b/homeassistant/components/linky/translations/hu.json deleted file mode 100644 index 9b450985375..00000000000 --- a/homeassistant/components/linky/translations/hu.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" - }, - "error": { - "access": "Nem siker\u00fclt el\u00e9rni az Enedis.fr webhelyet, ellen\u0151rizze internet-kapcsolat\u00e1t", - "enedis": "Az Enedis.fr hib\u00e1val v\u00e1laszolt: k\u00e9rj\u00fck, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra (\u00e1ltal\u00e1ban nem 23:00 \u00e9s 2:00 k\u00f6z\u00f6tt)", - "unknown": "Ismeretlen hiba: pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb (\u00e1ltal\u00e1ban nem 23:00 \u00e9s 2:00 \u00f3ra k\u00f6z\u00f6tt)" - }, - "step": { - "user": { - "data": { - "password": "Jelsz\u00f3", - "username": "E-mail" - }, - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/it.json b/homeassistant/components/linky/translations/it.json deleted file mode 100644 index ff5e226dcbe..00000000000 --- a/homeassistant/components/linky/translations/it.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Account gi\u00e0 configurato" - }, - "error": { - "access": "Impossibile accedere a Enedis.fr, si prega di controllare la connessione internet", - "enedis": "Enedis.fr ha risposto con un errore: si prega di riprovare pi\u00f9 tardi (di solito non tra le 23:00 e le 02:00).", - "unknown": "Errore sconosciuto: riprova pi\u00f9 tardi (in genere non tra le 23:00 e le 02:00)", - "wrong_login": "Errore di accesso: si prega di controllare la tua E-mail e la password" - }, - "step": { - "user": { - "data": { - "password": "Password", - "username": "E-mail" - }, - "description": "Inserisci le tue credenziali", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/ko.json b/homeassistant/components/linky/translations/ko.json deleted file mode 100644 index cd83aad724f..00000000000 --- a/homeassistant/components/linky/translations/ko.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." - }, - "error": { - "access": "Enedis.fr \uc5d0 \uc811\uc18d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc778\ud130\ub137 \uc5f0\uacb0\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694", - "enedis": "Enedis.fr \uc774 \uc624\ub958\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)", - "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)", - "wrong_login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" - }, - "step": { - "user": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc774\uba54\uc77c" - }, - "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/lb.json b/homeassistant/components/linky/translations/lb.json deleted file mode 100644 index 091a3b8d699..00000000000 --- a/homeassistant/components/linky/translations/lb.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Kont ass scho konfigur\u00e9iert" - }, - "error": { - "access": "Keng Verbindung zu Enedis.fr, iwwerpr\u00e9ift d'Internet Verbindung", - "enedis": "Enedis.fr huet mat engem Feeler ge\u00e4ntwert: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", - "unknown": "Onbekannte Feeler: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", - "wrong_login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert" - }, - "step": { - "user": { - "data": { - "password": "Passwuert", - "username": "E-Mail" - }, - "description": "F\u00ebllt \u00e4r Login Informatiounen aus", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/lv.json b/homeassistant/components/linky/translations/lv.json deleted file mode 100644 index 973833a5470..00000000000 --- a/homeassistant/components/linky/translations/lv.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "E-pasts" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/nl.json b/homeassistant/components/linky/translations/nl.json deleted file mode 100644 index 2c05353be3f..00000000000 --- a/homeassistant/components/linky/translations/nl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Account al geconfigureerd" - }, - "error": { - "access": "Geen toegang tot Enedis.fr, controleer uw internetverbinding", - "enedis": "Enedis.fr antwoordde met een fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", - "unknown": "Onbekende fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", - "wrong_login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord" - }, - "step": { - "user": { - "data": { - "password": "Wachtwoord", - "username": "E-mail" - }, - "description": "Voer uw gegevens in", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/nn.json b/homeassistant/components/linky/translations/nn.json deleted file mode 100644 index 6cdaaf837a4..00000000000 --- a/homeassistant/components/linky/translations/nn.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/no.json b/homeassistant/components/linky/translations/no.json deleted file mode 100644 index 5cf8ea2da34..00000000000 --- a/homeassistant/components/linky/translations/no.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Kontoen er allerede konfigurert" - }, - "error": { - "access": "Kunne ikke f\u00e5 tilgang til Enedis.fr, vennligst sjekk internettforbindelsen din", - "enedis": "Enedis.fr svarte med en feil: vennligst pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", - "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", - "wrong_login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt" - }, - "step": { - "user": { - "data": { - "password": "Passord", - "username": "E-post" - }, - "description": "Fyll inn legitimasjonen din", - "title": "" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/pl.json b/homeassistant/components/linky/translations/pl.json deleted file mode 100644 index 1fc09298fd7..00000000000 --- a/homeassistant/components/linky/translations/pl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." - }, - "error": { - "access": "Nie mo\u017cna uzyska\u0107 dost\u0119pu do Enedis.fr, sprawd\u017a po\u0142\u0105czenie internetowe", - "enedis": "Enedis.fr odpowiedzia\u0142 b\u0142\u0119dem: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy 23:00, a 2:00)", - "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy godzin\u0105 23:00, a 2:00)", - "wrong_login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o" - }, - "step": { - "user": { - "data": { - "password": "Has\u0142o", - "username": "Adres e-mail" - }, - "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/pt-BR.json b/homeassistant/components/linky/translations/pt-BR.json deleted file mode 100644 index bf2bc7070ae..00000000000 --- a/homeassistant/components/linky/translations/pt-BR.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "error": { - "wrong_login": "Erro de Login: por favor, verifique seu e-mail e senha" - }, - "step": { - "user": { - "data": { - "password": "Senha", - "username": "E-mail" - }, - "description": "Insira suas credenciais", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/pt.json b/homeassistant/components/linky/translations/pt.json deleted file mode 100644 index 54619af958e..00000000000 --- a/homeassistant/components/linky/translations/pt.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "Palavra-passe", - "username": "O email" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/ru.json b/homeassistant/components/linky/translations/ru.json deleted file mode 100644 index 65e0269967a..00000000000 --- a/homeassistant/components/linky/translations/ru.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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": { - "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.", - "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", - "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." - }, - "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" - }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/sl.json b/homeassistant/components/linky/translations/sl.json deleted file mode 100644 index 3df56ac5bbb..00000000000 --- a/homeassistant/components/linky/translations/sl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ra\u010dun \u017ee nastavljen" - }, - "error": { - "access": "Do Enedis.fr ni bilo mogo\u010de dostopati, preverite internetno povezavo", - "enedis": "Enedis.fr je odgovoril z napako: poskusite pozneje (ponavadi med 23. in 2. uro)", - "unknown": "Neznana napaka: Prosimo, poskusite pozneje (obi\u010dajno ne med 23. in 2. uro)", - "wrong_login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo" - }, - "step": { - "user": { - "data": { - "password": "Geslo", - "username": "E-po\u0161tni naslov" - }, - "description": "Vnesite svoje poverilnice", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/sv.json b/homeassistant/components/linky/translations/sv.json deleted file mode 100644 index 2d8c2b7177a..00000000000 --- a/homeassistant/components/linky/translations/sv.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Kontot har redan konfigurerats." - }, - "error": { - "access": "Det gick inte att komma \u00e5t Enedis.fr, kontrollera din internetanslutning", - "enedis": "Enedis.fr svarade med ett fel: f\u00f6rs\u00f6k igen senare (vanligtvis inte mellan 23:00 och 02:00)", - "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare (vanligtvis inte mellan 23:00 och 02:00)", - "wrong_login": "Inloggningsfel: v\u00e4nligen kontrollera din e-post och l\u00f6senord" - }, - "step": { - "user": { - "data": { - "password": "L\u00f6senord", - "username": "E-post" - }, - "description": "Ange dina autentiseringsuppgifter", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/zh-Hans.json b/homeassistant/components/linky/translations/zh-Hans.json deleted file mode 100644 index 62138856078..00000000000 --- a/homeassistant/components/linky/translations/zh-Hans.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "error": { - "wrong_login": "\u767b\u5f55\u51fa\u9519\uff1a\u8bf7\u68c0\u67e5\u60a8\u7684\u7535\u5b50\u90ae\u7bb1\u548c\u5bc6\u7801" - }, - "step": { - "user": { - "data": { - "password": "\u5bc6\u7801", - "username": "\u7535\u5b50\u90ae\u7bb1" - }, - "description": "\u8f93\u5165\u60a8\u7684\u8eab\u4efd\u8ba4\u8bc1" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/linky/translations/zh-Hant.json b/homeassistant/components/linky/translations/zh-Hant.json deleted file mode 100644 index 7a28dd692f6..00000000000 --- a/homeassistant/components/linky/translations/zh-Hant.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "access": "\u7121\u6cd5\u8a2a\u554f Enedis.fr\uff0c\u8acb\u6aa2\u67e5\u60a8\u7684\u7db2\u969b\u7db2\u8def\u9023\u7dda", - "enedis": "Endis.fr \u56de\u5831\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09", - "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09", - "wrong_login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u5bc6\u78bc" - }, - "step": { - "user": { - "data": { - "password": "\u5bc6\u78bc", - "username": "\u96fb\u5b50\u90f5\u4ef6" - }, - "description": "\u8f38\u5165\u6191\u8b49", - "title": "Linky" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7aa9eac6a86..3b4216377e5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -94,7 +94,6 @@ FLOWS = [ "konnected", "life360", "lifx", - "linky", "local_ip", "locative", "logi_circle", diff --git a/requirements_all.txt b/requirements_all.txt index d54cbea62eb..e4578335c1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,9 +1439,6 @@ pylgnetcast-homeassistant==0.2.0.dev0 # homeassistant.components.forked_daapd pylibrespot-java==0.1.0 -# homeassistant.components.linky -pylinky==0.4.0 - # homeassistant.components.litejet pylitejet==0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bf4f6ee196..6db238b23bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -676,9 +676,6 @@ pylast==3.2.1 # homeassistant.components.forked_daapd pylibrespot-java==0.1.0 -# homeassistant.components.linky -pylinky==0.4.0 - # homeassistant.components.litejet pylitejet==0.1 diff --git a/tests/components/linky/__init__.py b/tests/components/linky/__init__.py deleted file mode 100644 index f461885e384..00000000000 --- a/tests/components/linky/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Linky component.""" diff --git a/tests/components/linky/conftest.py b/tests/components/linky/conftest.py deleted file mode 100644 index 93e3ff78d2b..00000000000 --- a/tests/components/linky/conftest.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Linky generic test utils.""" -import pytest - -from tests.async_mock import patch - - -@pytest.fixture(autouse=True) -def patch_fakeuseragent(): - """Stub out fake useragent dep that makes requests.""" - with patch("pylinky.client.UserAgent", return_value="Test Browser"): - yield diff --git a/tests/components/linky/test_config_flow.py b/tests/components/linky/test_config_flow.py deleted file mode 100644 index f39f0da7d99..00000000000 --- a/tests/components/linky/test_config_flow.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Tests for the Linky config flow.""" -from pylinky.exceptions import ( - PyLinkyAccessException, - PyLinkyEnedisException, - PyLinkyException, - PyLinkyWrongLoginException, -) -import pytest - -from homeassistant import data_entry_flow -from homeassistant.components.linky.const import DEFAULT_TIMEOUT, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from homeassistant.helpers.typing import HomeAssistantType - -from tests.async_mock import Mock, patch -from tests.common import MockConfigEntry - -USERNAME = "username@hotmail.fr" -USERNAME_2 = "username@free.fr" -PASSWORD = "password" -TIMEOUT = 20 - - -@pytest.fixture(name="login") -def mock_controller_login(): - """Mock a successful login.""" - with patch( - "homeassistant.components.linky.config_flow.LinkyClient" - ) as service_mock: - service_mock.return_value.login = Mock(return_value=True) - service_mock.return_value.close_session = Mock(return_value=None) - yield service_mock - - -@pytest.fixture(name="fetch_data") -def mock_controller_fetch_data(): - """Mock a successful get data.""" - with patch( - "homeassistant.components.linky.config_flow.LinkyClient" - ) as service_mock: - service_mock.return_value.fetch_data = Mock(return_value={}) - service_mock.return_value.close_session = Mock(return_value=None) - yield service_mock - - -async def test_user(hass: HomeAssistantType, login, fetch_data): - """Test user config.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=None - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - # test with all provided - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == USERNAME - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_TIMEOUT] == DEFAULT_TIMEOUT - - -async def test_import(hass: HomeAssistantType, login, fetch_data): - """Test import step.""" - # import with username and password - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == USERNAME - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_TIMEOUT] == DEFAULT_TIMEOUT - - # import with all - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: USERNAME_2, - CONF_PASSWORD: PASSWORD, - CONF_TIMEOUT: TIMEOUT, - }, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == USERNAME_2 - assert result["title"] == USERNAME_2 - assert result["data"][CONF_USERNAME] == USERNAME_2 - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_TIMEOUT] == TIMEOUT - - -async def test_abort_if_already_setup(hass: HomeAssistantType, login, fetch_data): - """Test we abort if Linky is already setup.""" - MockConfigEntry( - domain=DOMAIN, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - unique_id=USERNAME, - ).add_to_hass(hass) - - # Should fail, same USERNAME (import) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - # Should fail, same USERNAME (flow) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_login_failed(hass: HomeAssistantType, login): - """Test when we have errors during login.""" - login.return_value.login.side_effect = PyLinkyAccessException() - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "access"} - hass.config_entries.flow.async_abort(result["flow_id"]) - - login.return_value.login.side_effect = PyLinkyWrongLoginException() - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "wrong_login"} - hass.config_entries.flow.async_abort(result["flow_id"]) - - -async def test_fetch_failed(hass: HomeAssistantType, login): - """Test when we have errors during fetch.""" - login.return_value.fetch_data.side_effect = PyLinkyAccessException() - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "access"} - hass.config_entries.flow.async_abort(result["flow_id"]) - - login.return_value.fetch_data.side_effect = PyLinkyEnedisException() - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "enedis"} - hass.config_entries.flow.async_abort(result["flow_id"]) - - login.return_value.fetch_data.side_effect = PyLinkyException() - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} - hass.config_entries.flow.async_abort(result["flow_id"]) From 20e85e1191dbd01b1ce0330aa57a6e236e1f7813 Mon Sep 17 00:00:00 2001 From: Dave Clarke <02DClarke@gmail.com> Date: Thu, 6 Aug 2020 10:40:50 +0100 Subject: [PATCH 008/862] Add support for Philips Hue Smart Button (#38555) * Add support for Philips Hue Smart Button * Fix linting with trailing commas * Update to correct deconz and hue model names/IDs --- homeassistant/components/deconz/device_trigger.py | 9 +++++++++ homeassistant/components/hue/device_trigger.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 6e5f4a11ca4..c343aa10fb1 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -91,6 +91,14 @@ HUE_DIMMER_REMOTE = { (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, } +HUE_BUTTON_REMOTE_MODEL = "ROM001" # Hue smart button +HUE_BUTTON_REMOTE = { + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1000}, + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, +} + HUE_TAP_REMOTE_MODEL = "ZGPSWITCH" HUE_TAP_REMOTE = { (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 34}, @@ -348,6 +356,7 @@ AQARA_OPPLE_6_BUTTONS = { REMOTES = { HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, + HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, FRIENDS_OF_HUE_SWITCH_MODEL: FRIENDS_OF_HUE_SWITCH, SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER, diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index 0c90c29f4c7..f3a8a57167a 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -52,6 +52,12 @@ HUE_DIMMER_REMOTE = { (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4003}, } +HUE_BUTTON_REMOTE_MODEL = "Hue Smart button" # ZLLSWITCH/ROM001 +HUE_BUTTON_REMOTE = { + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, +} + HUE_TAP_REMOTE_MODEL = "Hue tap switch" # ZGPSWITCH HUE_TAP_REMOTE = { (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 34}, @@ -80,6 +86,7 @@ HUE_FOHSWITCH_REMOTE = { REMOTES = { HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, + HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE, HUE_FOHSWITCH_REMOTE_MODEL: HUE_FOHSWITCH_REMOTE, } From e3e9ad1342b2588677022bdf03e75754327e85fa Mon Sep 17 00:00:00 2001 From: Markus Bong Date: Thu, 6 Aug 2020 12:14:39 +0200 Subject: [PATCH 009/862] Add devolo blinds devices (#36597) * add support for devolo blinds * correct internal sync function * correct naming * fix R1719 in line 73:15 * remove 'break point' print * simplified _sync check * change comment * change log msg --- .coveragerc | 1 + .../components/devolo_home_control/const.py | 2 +- .../components/devolo_home_control/cover.py | 99 +++++++++++++++++++ .../components/devolo_home_control/switch.py | 3 +- 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/devolo_home_control/cover.py diff --git a/.coveragerc b/.coveragerc index f340202cdb8..b716874c679 100644 --- a/.coveragerc +++ b/.coveragerc @@ -170,6 +170,7 @@ omit = homeassistant/components/devolo_home_control/__init__.py homeassistant/components/devolo_home_control/binary_sensor.py homeassistant/components/devolo_home_control/const.py + homeassistant/components/devolo_home_control/cover.py homeassistant/components/devolo_home_control/devolo_device.py homeassistant/components/devolo_home_control/devolo_multi_level_switch.py homeassistant/components/devolo_home_control/light.py diff --git a/homeassistant/components/devolo_home_control/const.py b/homeassistant/components/devolo_home_control/const.py index 60923235916..b98346539d0 100644 --- a/homeassistant/components/devolo_home_control/const.py +++ b/homeassistant/components/devolo_home_control/const.py @@ -3,6 +3,6 @@ DOMAIN = "devolo_home_control" DEFAULT_MYDEVOLO = "https://www.mydevolo.com" DEFAULT_MPRM = "https://homecontrol.mydevolo.com" -PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] CONF_MYDEVOLO = "mydevolo_url" CONF_HOMECONTROL = "home_control_url" diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py new file mode 100644 index 00000000000..7dae0c605ca --- /dev/null +++ b/homeassistant/components/devolo_home_control/cover.py @@ -0,0 +1,99 @@ +"""Platform for cover integration.""" +import logging + +from homeassistant.components.cover import ( + DEVICE_CLASS_BLIND, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DOMAIN +from .devolo_device import DevoloDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Get all cover devices and setup them via config entry.""" + entities = [] + + for device in hass.data[DOMAIN]["homecontrol"].multi_level_switch_devices: + for multi_level_switch in device.multi_level_switch_property: + if multi_level_switch.startswith("devolo.Blinds"): + entities.append( + DevoloCoverDeviceEntity( + homecontrol=hass.data[DOMAIN]["homecontrol"], + device_instance=device, + element_uid=multi_level_switch, + ) + ) + + async_add_entities(entities, False) + + +class DevoloCoverDeviceEntity(DevoloDeviceEntity, CoverEntity): + """Representation of a cover device within devolo Home Control.""" + + def __init__(self, homecontrol, device_instance, element_uid): + """Initialize a devolo blinds device.""" + super().__init__( + homecontrol=homecontrol, + device_instance=device_instance, + element_uid=element_uid, + name=device_instance.itemName, + sync=self._sync, + ) + + self._multi_level_switch_property = device_instance.multi_level_switch_property.get( + element_uid + ) + + self._position = self._multi_level_switch_property.value + + @property + def current_cover_position(self): + """Return the current position. 0 is closed. 100 is open.""" + return self._position + + @property + def device_class(self): + """Return the class of the device.""" + return DEVICE_CLASS_BLIND + + @property + def is_closed(self): + """Return if the blind is closed or not.""" + return not bool(self._position) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + def open_cover(self, **kwargs): + """Open the blind.""" + self._multi_level_switch_property.set(100) + + def close_cover(self, **kwargs): + """Close the blind.""" + self._multi_level_switch_property.set(0) + + def set_cover_position(self, **kwargs): + """Set the blind to the given position.""" + self._multi_level_switch_property.set(kwargs["position"]) + + def _sync(self, message=None): + """Update the binary sensor state.""" + if message[0] == self._unique_id: + self._position = message[1] + elif message[0].startswith("hdm"): + self._available = self._device_instance.is_online() + else: + _LOGGER.debug("Not valid message received: %s", message) + self.schedule_update_ha_state() diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index d14b6c059de..4ba212af379 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -19,7 +19,8 @@ async def async_setup_entry( entities = [] for device in devices: for binary_switch in device.binary_switch_property: - # Exclude the binary switch which have also a multi_level_switches here, because they are implemented as light devices now. + # Exclude the binary switch which also has multi_level_switches here, + # because those are implemented as light entities now. if not hasattr(device, "multi_level_switch_property"): entities.append( DevoloSwitch( From 95835326f30403f5cbe45e58c5cfc18464477a6d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 6 Aug 2020 12:36:59 +0200 Subject: [PATCH 010/862] Do not print warning when command line switch queries off (#38591) --- homeassistant/components/command_line/__init__.py | 11 ++++++++--- homeassistant/components/command_line/switch.py | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 92f219a13ea..4f98818d9b3 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -6,8 +6,12 @@ import subprocess _LOGGER = logging.getLogger(__name__) -def call_shell_with_timeout(command, timeout): - """Run a shell command with a timeout.""" +def call_shell_with_timeout(command, timeout, *, log_return_code=True): + """Run a shell command with a timeout. + + If log_return_code is set to False, it will not print an error if a non-zero + return code is returned. + """ try: _LOGGER.debug("Running command: %s", command) subprocess.check_output( @@ -15,7 +19,8 @@ def call_shell_with_timeout(command, timeout): ) return 0 except subprocess.CalledProcessError as proc_exception: - _LOGGER.error("Command failed: %s", command) + if log_return_code: + _LOGGER.error("Command failed: %s", command) return proc_exception.returncode except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", command) diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 50cda31a537..804e3c6a4d5 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -114,7 +114,9 @@ class CommandSwitch(SwitchEntity): def _query_state_code(self, command): """Execute state command for return code.""" _LOGGER.info("Running state code command: %s", command) - return call_shell_with_timeout(command, self._timeout) == 0 + return ( + call_shell_with_timeout(command, self._timeout, log_return_code=False) == 0 + ) @property def should_poll(self): From aaad9860021b45a08c663162cfcd38d414018f94 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 6 Aug 2020 12:54:18 +0200 Subject: [PATCH 011/862] Improve Xioami Aqara zeroconf discovery handling (#37469) Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- .../components/xiaomi_aqara/__init__.py | 2 +- .../components/xiaomi_aqara/config_flow.py | 108 +++++++++---- .../components/xiaomi_aqara/strings.json | 11 +- .../xiaomi_aqara/translations/en.json | 11 +- .../xiaomi_aqara/test_config_flow.py | 150 ++++++++++++++---- 5 files changed, 209 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index d759785f49f..c5b74e68af5 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -142,11 +142,11 @@ async def async_setup_entry( xiaomi_gateway = await hass.async_add_executor_job( XiaomiGateway, entry.data[CONF_HOST], - entry.data[CONF_PORT], entry.data[CONF_SID], entry.data[CONF_KEY], DEFAULT_DISCOVERY_RETRY, entry.data[CONF_INTERFACE], + entry.data[CONF_PORT], entry.data[CONF_PROTOCOL], ) hass.data[DOMAIN][GATEWAYS_KEY][entry.entry_id] = xiaomi_gateway diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index b9cfe58ac4b..fb66be76635 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -3,10 +3,11 @@ import logging from socket import gaierror import voluptuous as vol -from xiaomi_gateway import XiaomiGatewayDiscovery +from xiaomi_gateway import MULTICAST_PORT, XiaomiGateway, XiaomiGatewayDiscovery from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.core import callback from homeassistant.helpers.device_registry import format_mac # pylint: disable=unused-import @@ -15,6 +16,7 @@ from .const import ( CONF_KEY, CONF_PROTOCOL, CONF_SID, + DEFAULT_DISCOVERY_RETRY, DOMAIN, ZEROCONF_GATEWAY, ) @@ -28,6 +30,11 @@ DEFAULT_INTERFACE = "any" GATEWAY_CONFIG = vol.Schema( {vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): str} ) +CONFIG_HOST = { + vol.Optional(CONF_HOST): str, + vol.Optional(CONF_MAC): str, +} +GATEWAY_CONFIG_HOST = GATEWAY_CONFIG.extend(CONFIG_HOST) GATEWAY_SETTINGS = vol.Schema( { vol.Optional(CONF_KEY): vol.All(str, vol.Length(min=16, max=16)), @@ -46,44 +53,78 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize.""" self.host = None self.interface = DEFAULT_INTERFACE + self.sid = None self.gateways = None self.selected_gateway = None + @callback + def async_show_form_step_user(self, errors): + """Show the form belonging to the user step.""" + schema = GATEWAY_CONFIG + if (self.host is None and self.sid is None) or errors: + schema = GATEWAY_CONFIG_HOST + + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} - if user_input is not None: - self.interface = user_input[CONF_INTERFACE] + if user_input is None: + return self.async_show_form_step_user(errors) - # Discover Xiaomi Aqara Gateways in the netwerk to get required SIDs. - xiaomi = XiaomiGatewayDiscovery(self.hass.add_job, [], self.interface) - try: - await self.hass.async_add_executor_job(xiaomi.discover_gateways) - except gaierror: - errors[CONF_INTERFACE] = "invalid_interface" + self.interface = user_input[CONF_INTERFACE] - if not errors: - self.gateways = xiaomi.gateways + # allow optional manual setting of host and mac + if self.host is None and self.sid is None: + self.host = user_input.get(CONF_HOST) + mac_address = user_input.get(CONF_MAC) - # if host is already known by zeroconf discovery - if self.host is not None: - self.selected_gateway = self.gateways.get(self.host) - if self.selected_gateway is not None: - return await self.async_step_settings() + # format sid from mac_address + if mac_address is not None: + self.sid = format_mac(mac_address).replace(":", "") - errors["base"] = "not_found_error" - else: - if len(self.gateways) == 1: - self.selected_gateway = list(self.gateways.values())[0] - return await self.async_step_settings() - if len(self.gateways) > 1: - return await self.async_step_select() + # if host is already known by zeroconf discovery or manual optional settings + if self.host is not None and self.sid is not None: + # Connect to Xiaomi Aqara Gateway + self.selected_gateway = await self.hass.async_add_executor_job( + XiaomiGateway, + self.host, + self.sid, + None, + DEFAULT_DISCOVERY_RETRY, + self.interface, + MULTICAST_PORT, + None, + ) - errors["base"] = "discovery_error" + if self.selected_gateway.connection_error: + errors[CONF_HOST] = "invalid_host" + if self.selected_gateway.mac_error: + errors[CONF_MAC] = "invalid_mac" + if errors: + return self.async_show_form_step_user(errors) - return self.async_show_form( - step_id="user", data_schema=GATEWAY_CONFIG, errors=errors - ) + return await self.async_step_settings() + + # Discover Xiaomi Aqara Gateways in the netwerk to get required SIDs. + xiaomi = XiaomiGatewayDiscovery(self.hass.add_job, [], self.interface) + try: + await self.hass.async_add_executor_job(xiaomi.discover_gateways) + except gaierror: + errors[CONF_INTERFACE] = "invalid_interface" + return self.async_show_form_step_user(errors) + + self.gateways = xiaomi.gateways + + if len(self.gateways) == 1: + self.selected_gateway = list(self.gateways.values())[0] + self.sid = self.selected_gateway.sid + return await self.async_step_settings() + if len(self.gateways) > 1: + return await self.async_step_select() + + errors["base"] = "discovery_error" + return self.async_show_form_step_user(errors) async def async_step_select(self, user_input=None): """Handle multiple aqara gateways found.""" @@ -91,6 +132,7 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: ip_adress = user_input["select_ip"] self.selected_gateway = self.gateways[ip_adress] + self.sid = self.selected_gateway.sid return await self.async_step_settings() select_schema = vol.Schema( @@ -123,9 +165,12 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="not_xiaomi_aqara") - # format mac (include semicolns and make uppercase) + # format mac (include semicolns and make lowercase) mac_address = format_mac(mac_address) + # format sid from mac_address + self.sid = mac_address.replace(":", "") + unique_id = mac_address await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) @@ -144,19 +189,18 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): key = user_input.get(CONF_KEY) ip_adress = self.selected_gateway.ip_adress port = self.selected_gateway.port - sid = self.selected_gateway.sid protocol = self.selected_gateway.proto if key is not None: # validate key by issuing stop ringtone playback command. self.selected_gateway.key = key - valid_key = self.selected_gateway.write_to_hub(sid, mid=10000) + valid_key = self.selected_gateway.write_to_hub(self.sid, mid=10000) else: valid_key = True if valid_key: # format_mac, for a gateway the sid equels the mac address - mac_address = format_mac(sid) + mac_address = format_mac(self.sid) # set unique_id unique_id = mac_address @@ -172,7 +216,7 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_INTERFACE: self.interface, CONF_PROTOCOL: protocol, CONF_KEY: key, - CONF_SID: sid, + CONF_SID: self.sid, }, ) diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index 87e1d37cb93..5cbdc91a661 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -4,9 +4,11 @@ "step": { "user": { "title": "Xiaomi Aqara Gateway", - "description": "Connect to your Xiaomi Aqara Gateway", + "description": "Connect to your Xiaomi Aqara Gateway, if the IP and mac addresses are left empty, auto-discovery is used", "data": { - "interface": "The network interface to use" + "interface": "The network interface to use", + "host": "[%key:common::config_flow::data::ip%] (optional)", + "mac": "Mac Address (optional)" } }, "settings": { @@ -27,9 +29,10 @@ }, "error": { "discovery_error": "Failed to discover a Xiaomi Aqara Gateway, try using the IP of the device running HomeAssistant as interface", - "not_found_error": "Zeroconf discovered Gateway could not be located to get the necessary information, try using the IP of the device running HomeAssistant as interface", "invalid_interface": "Invalid network interface", - "invalid_key": "Invalid gateway key" + "invalid_key": "Invalid gateway key", + "invalid_host": "Invalid [%key:common::config_flow::data::ip%]", + "invalid_mac": "Invalid Mac Address" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json index 7b801e33089..b9f6fa7ab2a 100644 --- a/homeassistant/components/xiaomi_aqara/translations/en.json +++ b/homeassistant/components/xiaomi_aqara/translations/en.json @@ -1,15 +1,16 @@ { "config": { "abort": { - "already_configured": "Device is already configured", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "Config flow for this gateway is already in progress", "not_xiaomi_aqara": "Not a Xiaomi Aqara Gateway, discovered device did not match known gateways" }, "error": { "discovery_error": "Failed to discover a Xiaomi Aqara Gateway, try using the IP of the device running HomeAssistant as interface", + "invalid_host": "Invalid [%key:common::config_flow::data::ip%]", "invalid_interface": "Invalid network interface", "invalid_key": "Invalid gateway key", - "not_found_error": "Zeroconf discovered Gateway could not be located to get the necessary information, try using the IP of the device running HomeAssistant as interface" + "invalid_mac": "Invalid Mac Address" }, "flow_title": "Xiaomi Aqara Gateway: {name}", "step": { @@ -30,9 +31,11 @@ }, "user": { "data": { - "interface": "The network interface to use" + "host": "[%key:common::config_flow::data::ip%] (optional)", + "interface": "The network interface to use", + "mac": "Mac Address (optional)" }, - "description": "Connect to your Xiaomi Aqara Gateway", + "description": "Connect to your Xiaomi Aqara Gateway, if the IP and mac addresses are left empty, auto-discovery is used", "title": "Xiaomi Aqara Gateway" } } diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index b7762317fdf..06fda84c934 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -34,13 +34,22 @@ def xiaomi_aqara_fixture(): with patch( "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery", return_value=mock_gateway_discovery, + ), patch( + "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGateway", + return_value=mock_gateway_discovery.gateways[TEST_HOST], ), patch( "homeassistant.components.xiaomi_aqara.async_setup_entry", return_value=True ): yield -def get_mock_discovery(host_list, invalid_interface=False, invalid_key=False): +def get_mock_discovery( + host_list, + invalid_interface=False, + invalid_key=False, + invalid_host=False, + invalid_mac=False, +): """Return a mock gateway info instance.""" gateway_discovery = Mock() @@ -52,6 +61,8 @@ def get_mock_discovery(host_list, invalid_interface=False, invalid_key=False): gateway.port = TEST_PORT gateway.sid = TEST_SID gateway.proto = TEST_PROTOCOL + gateway.connection_error = invalid_host + gateway.mac_error = invalid_mac if invalid_key: gateway.write_to_hub = Mock(return_value=False) @@ -185,6 +196,52 @@ async def test_config_flow_user_no_key_success(hass): } +async def test_config_flow_user_host_mac_success(hass): + """Test a successful config flow initialized by the user with a host and mac specified.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + mock_gateway_discovery = get_mock_discovery([]) + + with patch( + "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery", + return_value=mock_gateway_discovery, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE, + CONF_HOST: TEST_HOST, + CONF_MAC: TEST_MAC, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "settings" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: TEST_NAME}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + CONF_MAC: TEST_MAC, + const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE, + const.CONF_PROTOCOL: TEST_PROTOCOL, + const.CONF_KEY: None, + const.CONF_SID: TEST_SID, + } + + async def test_config_flow_user_discovery_error(hass): """Test a failed config flow initialized by the user with no gateways discoverd.""" result = await hass.config_entries.flow.async_init( @@ -235,6 +292,66 @@ async def test_config_flow_user_invalid_interface(hass): assert result["errors"] == {const.CONF_INTERFACE: "invalid_interface"} +async def test_config_flow_user_invalid_host(hass): + """Test a failed config flow initialized by the user with an invalid host.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + mock_gateway_discovery = get_mock_discovery([TEST_HOST], invalid_host=True) + + with patch( + "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGateway", + return_value=mock_gateway_discovery.gateways[TEST_HOST], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE, + CONF_HOST: "0.0.0.0", + CONF_MAC: TEST_MAC, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"host": "invalid_host"} + + +async def test_config_flow_user_invalid_mac(hass): + """Test a failed config flow initialized by the user with an invalid mac.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + mock_gateway_discovery = get_mock_discovery([TEST_HOST], invalid_mac=True) + + with patch( + "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGateway", + return_value=mock_gateway_discovery.gateways[TEST_HOST], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE, + CONF_HOST: TEST_HOST, + CONF_MAC: "in:va:li:d0:0m:ac", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"mac": "invalid_mac"} + + async def test_config_flow_user_invalid_key(hass): """Test a failed config flow initialized by the user with an invalid key.""" result = await hass.config_entries.flow.async_init( @@ -335,34 +452,3 @@ async def test_zeroconf_unknown_device(hass): assert result["type"] == "abort" assert result["reason"] == "not_xiaomi_aqara" - - -async def test_zeroconf_not_found_error(hass): - """Test a failed zeroconf discovery because the correct gateway could not be found.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - zeroconf.ATTR_HOST: TEST_HOST, - ZEROCONF_NAME: TEST_ZEROCONF_NAME, - ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC}, - }, - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - assert result["errors"] == {} - - mock_gateway_discovery = get_mock_discovery([TEST_HOST_2]) - - with patch( - "homeassistant.components.xiaomi_aqara.config_flow.XiaomiGatewayDiscovery", - return_value=mock_gateway_discovery, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - assert result["errors"] == {"base": "not_found_error"} From 39e6bca682832ce6435152e52f4d396bca2489e1 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 6 Aug 2020 15:58:26 +0200 Subject: [PATCH 012/862] Support Next/Previous for InputSelector (#38378) * Support Next/Previous for InputSelector * Update homeassistant/components/google_assistant/trait.py Co-authored-by: Paulus Schoutsen * Adjust to match new version of _next_selected Co-authored-by: Paulus Schoutsen --- .../components/google_assistant/const.py | 1 + .../components/google_assistant/trait.py | 35 +++++++- .../components/google_assistant/test_trait.py | 82 +++++++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index c1b8d704608..88b704eb518 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -97,6 +97,7 @@ ERR_NOT_SUPPORTED = "notSupported" ERR_PROTOCOL_ERROR = "protocolError" ERR_UNKNOWN_ERROR = "unknownError" ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported" +ERR_UNSUPPORTED_INPUT = "unsupportedInput" ERR_ALREADY_DISARMED = "alreadyDisarmed" ERR_ALREADY_ARMED = "alreadyArmed" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 90b5016260d..9afdff4045f 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1,5 +1,6 @@ """Implement the Google Smart Home traits.""" import logging +from typing import List, Optional from homeassistant.components import ( alarm_control_panel, @@ -68,6 +69,7 @@ from .const import ( ERR_CHALLENGE_NOT_SETUP, ERR_FUNCTION_NOT_SUPPORTED, ERR_NOT_SUPPORTED, + ERR_UNSUPPORTED_INPUT, ERR_VALUE_OUT_OF_RANGE, ) from .error import ChallengeNeeded, SmartHomeError @@ -114,6 +116,8 @@ COMMAND_LOCKUNLOCK = f"{PREFIX_COMMANDS}LockUnlock" COMMAND_FANSPEED = f"{PREFIX_COMMANDS}SetFanSpeed" COMMAND_MODES = f"{PREFIX_COMMANDS}SetModes" COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput" +COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput" +COMMAND_PREVIOUS_INPUT = f"{PREFIX_COMMANDS}PreviousInput" COMMAND_OPENCLOSE = f"{PREFIX_COMMANDS}OpenClose" COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume" COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative" @@ -145,6 +149,20 @@ def _google_temp_unit(units): return "C" +def _next_selected(items: List[str], selected: Optional[str]) -> Optional[str]: + """Return the next item in a item list starting at given value. + + If selected is missing in items, None is returned + """ + try: + index = items.index(selected) + except ValueError: + return None + + next_item = 0 if index == len(items) - 1 else index + 1 + return items[next_item] + + class _Trait: """Represents a Trait inside Google Assistant skill.""" @@ -1395,7 +1413,7 @@ class InputSelectorTrait(_Trait): """ name = TRAIT_INPUTSELECTOR - commands = [COMMAND_INPUT] + commands = [COMMAND_INPUT, COMMAND_NEXT_INPUT, COMMAND_PREVIOUS_INPUT] SYNONYMS = {} @@ -1428,7 +1446,20 @@ class InputSelectorTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an SetInputSource command.""" - requested_source = params.get("newInput") + sources = self.state.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or [] + source = self.state.attributes.get(media_player.ATTR_INPUT_SOURCE) + + if command == COMMAND_INPUT: + requested_source = params.get("newInput") + elif command == COMMAND_NEXT_INPUT: + requested_source = _next_selected(sources, source) + elif command == COMMAND_PREVIOUS_INPUT: + requested_source = _next_selected(list(reversed(sources)), source) + else: + raise SmartHomeError(ERR_NOT_SUPPORTED, "Unsupported command") + + if requested_source not in sources: + raise SmartHomeError(ERR_UNSUPPORTED_INPUT, "Unsupported input") await self.hass.services.async_call( media_player.DOMAIN, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index faad53fbc66..0ca53d256a4 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -24,6 +24,7 @@ from homeassistant.components import ( ) from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import const, error, helpers, trait +from homeassistant.components.google_assistant.error import SmartHomeError from homeassistant.components.humidifier import const as humidifier from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( @@ -1428,6 +1429,87 @@ async def test_inputselector(hass): assert calls[0].data == {"entity_id": "media_player.living_room", "source": "media"} +@pytest.mark.parametrize( + "sources,source,source_next,source_prev", + [ + (["a"], "a", "a", "a"), + (["a", "b"], "a", "b", "b"), + (["a", "b", "c"], "a", "b", "c"), + ], +) +async def test_inputselector_nextprev(hass, sources, source, source_next, source_prev): + """Test input selector trait.""" + trt = trait.InputSelectorTrait( + hass, + State( + "media_player.living_room", + media_player.STATE_PLAYING, + attributes={ + media_player.ATTR_INPUT_SOURCE_LIST: sources, + media_player.ATTR_INPUT_SOURCE: source, + }, + ), + BASIC_CONFIG, + ) + + assert trt.can_execute("action.devices.commands.NextInput", params={}) + assert trt.can_execute("action.devices.commands.PreviousInput", params={}) + + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE + ) + await trt.execute( + "action.devices.commands.NextInput", BASIC_DATA, {}, {}, + ) + await trt.execute( + "action.devices.commands.PreviousInput", BASIC_DATA, {}, {}, + ) + + assert len(calls) == 2 + assert calls[0].data == { + "entity_id": "media_player.living_room", + "source": source_next, + } + assert calls[1].data == { + "entity_id": "media_player.living_room", + "source": source_prev, + } + + +@pytest.mark.parametrize( + "sources,source", [(None, "a"), (["a", "b"], None), (["a", "b"], "c")] +) +async def test_inputselector_nextprev_invalid(hass, sources, source): + """Test input selector trait.""" + trt = trait.InputSelectorTrait( + hass, + State( + "media_player.living_room", + media_player.STATE_PLAYING, + attributes={ + media_player.ATTR_INPUT_SOURCE_LIST: sources, + media_player.ATTR_INPUT_SOURCE: source, + }, + ), + BASIC_CONFIG, + ) + + with pytest.raises(SmartHomeError): + await trt.execute( + "action.devices.commands.NextInput", BASIC_DATA, {}, {}, + ) + + with pytest.raises(SmartHomeError): + await trt.execute( + "action.devices.commands.PreviousInput", BASIC_DATA, {}, {}, + ) + + with pytest.raises(SmartHomeError): + await trt.execute( + "action.devices.commands.InvalidCommand", BASIC_DATA, {}, {}, + ) + + async def test_modes_input_select(hass): """Test Input Select Mode trait.""" assert helpers.get_google_type(input_select.DOMAIN, None) is not None From 988d3e93736609e69cf6c75e796b2981087780c5 Mon Sep 17 00:00:00 2001 From: DelusionalAI <59485008+DelusionalAI@users.noreply.github.com> Date: Thu, 6 Aug 2020 09:50:51 -0500 Subject: [PATCH 013/862] Add node firmware to ozw device registry (#38330) --- homeassistant/components/ozw/__init__.py | 1 - homeassistant/components/ozw/entity.py | 8 ++++++++ homeassistant/components/ozw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index bb48c180971..bbac0e843e9 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -114,7 +114,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Filter out CommandClasses we're definitely not interested in. if value.command_class in [ - CommandClass.VERSION, CommandClass.MANUFACTURER_SPECIFIC, ]: return diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py index deb70af1bb5..ffec0feff07 100644 --- a/homeassistant/components/ozw/entity.py +++ b/homeassistant/components/ozw/entity.py @@ -7,6 +7,8 @@ from openzwavemqtt.const import ( EVENT_INSTANCE_STATUS_CHANGED, EVENT_VALUE_CHANGED, OZW_READY_STATES, + CommandClass, + ValueIndex, ) from openzwavemqtt.models.node import OZWNode from openzwavemqtt.models.value import OZWValue @@ -182,12 +184,18 @@ class ZWaveDeviceEntity(Entity): node = self.values.primary.node node_instance = self.values.primary.instance dev_id = create_device_id(node, self.values.primary.instance) + node_firmware = node.get_value( + CommandClass.VERSION, ValueIndex.VERSION_APPLICATION + ) device_info = { "identifiers": {(DOMAIN, dev_id)}, "name": create_device_name(node), "manufacturer": node.node_manufacturer_name, "model": node.node_product_name, } + if node_firmware is not None: + device_info["sw_version"] = node_firmware.value + # device with multiple instances is split up into virtual devices for each instance if node_instance > 1: parent_dev_id = create_device_id(node) diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index c6c96ed15a2..d2cf4772bb1 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ozw", "requirements": [ - "python-openzwave-mqtt==1.0.2" + "python-openzwave-mqtt==1.0.4" ], "after_dependencies": [ "mqtt" diff --git a/requirements_all.txt b/requirements_all.txt index e4578335c1f..980730ad845 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1734,7 +1734,7 @@ python-nest==4.1.0 python-nmap==0.6.1 # homeassistant.components.ozw -python-openzwave-mqtt==1.0.2 +python-openzwave-mqtt==1.0.4 # homeassistant.components.qbittorrent python-qbittorrent==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6db238b23bc..4736d5d0046 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -791,7 +791,7 @@ python-miio==0.5.3 python-nest==4.1.0 # homeassistant.components.ozw -python-openzwave-mqtt==1.0.2 +python-openzwave-mqtt==1.0.4 # homeassistant.components.songpal python-songpal==0.12 From 0cdd47b014720f24e5aba531a68286fb4ee30371 Mon Sep 17 00:00:00 2001 From: Kendell R Date: Thu, 6 Aug 2020 08:21:45 -0700 Subject: [PATCH 014/862] Change http to auto for cast media image url (#38242) * Change http to auto * Update media_player.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py * Format --- homeassistant/components/cast/media_player.py | 4 +- tests/components/cast/test_media_player.py | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 44b6bf451c1..9aec9a47ea2 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -602,7 +602,9 @@ class CastDevice(MediaPlayerEntity): images = media_status.images - return images[0].url if images and images[0].url else None + return ( + images[0].url.replace("http://", "//") if images and images[0].url else None + ) @property def media_image_remotely_accessible(self) -> bool: diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 1338eb0f0cc..ad18430c37a 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -440,6 +440,45 @@ async def test_entity_media_states(hass: HomeAssistantType): assert state.state == "unknown" +async def test_url_replace(hass: HomeAssistantType): + """Test functionality of replacing URL for HTTPS.""" + info = get_fake_chromecast_info() + full_info = attr.evolve( + info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID + ) + + chromecast, entity = await async_setup_media_player_cast(hass, info) + + entity._available = True + entity.schedule_update_ha_state() + await hass.async_block_till_done() + + state = hass.states.get("media_player.speaker") + assert state is not None + assert state.name == "Speaker" + assert state.state == "unknown" + assert entity.unique_id == full_info.uuid + + class FakeHTTPImage: + url = "http://example.com/test.png" + + class FakeHTTPSImage: + url = "https://example.com/test.png" + + media_status = MagicMock(images=[FakeHTTPImage()]) + media_status.player_is_playing = True + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get("media_player.speaker") + assert state.attributes.get("entity_picture") == "//example.com/test.png" + + media_status.images = [FakeHTTPSImage()] + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get("media_player.speaker") + assert state.attributes.get("entity_picture") == "https://example.com/test.png" + + async def test_group_media_states(hass: HomeAssistantType): """Test media states are read from group if entity has no state.""" info = get_fake_chromecast_info() From fd52ff531d80f6e69e5d76734dc4fb88b94d4898 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 6 Aug 2020 19:02:32 +0200 Subject: [PATCH 015/862] Remove wrong update per core design on ZHA (#38599) --- homeassistant/components/zha/entity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index d583f89c9bc..695a4f6ca6a 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -256,7 +256,6 @@ class ZhaGroupEntity(BaseZhaEntity): ) self.async_on_remove(send_removed_signal) - await self.async_update() @callback def async_state_changed_listener(self, event: Event): From ae40f87a5c319ec8171c6cd565eb830423b05dd4 Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Thu, 6 Aug 2020 21:40:54 +0300 Subject: [PATCH 016/862] Add events to Dynalite platform (#38583) Co-authored-by: Martin Hjelmare --- homeassistant/components/dynalite/bridge.py | 41 +++++++++++++- homeassistant/components/dynalite/const.py | 5 ++ .../components/dynalite/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dynalite/test_bridge.py | 53 +++++++++++++++++++ 6 files changed, 100 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py index 522061a85aa..5bf21801b3d 100644 --- a/homeassistant/components/dynalite/bridge.py +++ b/homeassistant/components/dynalite/bridge.py @@ -2,13 +2,28 @@ from typing import Any, Callable, Dict, List, Optional -from dynalite_devices_lib.dynalite_devices import DynaliteBaseDevice, DynaliteDevices +from dynalite_devices_lib.dynalite_devices import ( + CONF_AREA as dyn_CONF_AREA, + CONF_PRESET as dyn_CONF_PRESET, + NOTIFICATION_PACKET, + NOTIFICATION_PRESET, + DynaliteBaseDevice, + DynaliteDevices, + DynaliteNotification, +) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ENTITY_PLATFORMS, LOGGER +from .const import ( + ATTR_AREA, + ATTR_HOST, + ATTR_PACKET, + ATTR_PRESET, + ENTITY_PLATFORMS, + LOGGER, +) from .convert_config import convert_config @@ -26,6 +41,7 @@ class DynaliteBridge: self.dynalite_devices = DynaliteDevices( new_device_func=self.add_devices_when_registered, update_device_func=self.update_device, + notification_func=self.handle_notification, ) self.dynalite_devices.configure(convert_config(config)) @@ -61,6 +77,27 @@ class DynaliteBridge: else: async_dispatcher_send(self.hass, self.update_signal(device)) + @callback + def handle_notification(self, notification: DynaliteNotification) -> None: + """Handle a notification from the platform and issue events.""" + if notification.notification == NOTIFICATION_PACKET: + self.hass.bus.async_fire( + "dynalite_packet", + { + ATTR_HOST: self.host, + ATTR_PACKET: notification.data[NOTIFICATION_PACKET], + }, + ) + if notification.notification == NOTIFICATION_PRESET: + self.hass.bus.async_fire( + "dynalite_preset", + { + ATTR_HOST: self.host, + ATTR_AREA: notification.data[dyn_CONF_AREA], + ATTR_PRESET: notification.data[dyn_CONF_PRESET], + }, + ) + @callback def register_add_devices(self, platform: str, async_add_devices: Callable) -> None: """Add an async_add_entities for a category.""" diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index e5a4e90d1bd..373e64a1b76 100644 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -49,3 +49,8 @@ DEFAULT_TEMPLATES = { CONF_TILT_TIME, ], } + +ATTR_AREA = "area" +ATTR_HOST = "host" +ATTR_PACKET = "packet" +ATTR_PRESET = "preset" diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index e09410e7ef5..a8277eea85c 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -4,5 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.41"] + "requirements": ["dynalite_devices==0.1.44"] } diff --git a/requirements_all.txt b/requirements_all.txt index 980730ad845..eb468e2cdf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -503,7 +503,7 @@ dsmr_parser==0.18 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.41 +dynalite_devices==0.1.44 # homeassistant.components.rainforest_eagle eagle200_reader==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4736d5d0046..597742a0d6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -258,7 +258,7 @@ doorbirdpy==2.0.8 dsmr_parser==0.18 # homeassistant.components.dynalite -dynalite_devices==0.1.41 +dynalite_devices==0.1.44 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index ea73f75a390..8f3210bbcf8 100644 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -1,6 +1,21 @@ """Test Dynalite bridge.""" + +from dynalite_devices_lib.dynalite_devices import ( + CONF_AREA as dyn_CONF_AREA, + CONF_PRESET as dyn_CONF_PRESET, + NOTIFICATION_PACKET, + NOTIFICATION_PRESET, + DynaliteNotification, +) + from homeassistant.components import dynalite +from homeassistant.components.dynalite.const import ( + ATTR_AREA, + ATTR_HOST, + ATTR_PACKET, + ATTR_PRESET, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from tests.async_mock import AsyncMock, Mock, patch @@ -95,3 +110,41 @@ async def test_register_then_add_devices(hass): await hass.async_block_till_done() assert hass.states.get("light.name") assert hass.states.get("switch.name2") + + +async def test_notifications(hass): + """Test that update works.""" + host = "1.2.3.4" + entry = MockConfigEntry(domain=dynalite.DOMAIN, data={dynalite.CONF_HOST: host}) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices" + ) as mock_dyn_dev: + mock_dyn_dev().async_setup = AsyncMock(return_value=True) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + notification_func = mock_dyn_dev.mock_calls[1][2]["notification_func"] + event_listener1 = Mock() + hass.bus.async_listen("dynalite_packet", event_listener1) + packet = [5, 4, 3, 2] + notification_func( + DynaliteNotification(NOTIFICATION_PACKET, {NOTIFICATION_PACKET: packet}) + ) + await hass.async_block_till_done() + event_listener1.assert_called_once() + my_event = event_listener1.mock_calls[0][1][0] + assert my_event.data[ATTR_HOST] == host + assert my_event.data[ATTR_PACKET] == packet + event_listener2 = Mock() + hass.bus.async_listen("dynalite_preset", event_listener2) + notification_func( + DynaliteNotification( + NOTIFICATION_PRESET, {dyn_CONF_AREA: 7, dyn_CONF_PRESET: 2} + ) + ) + await hass.async_block_till_done() + event_listener2.assert_called_once() + my_event = event_listener2.mock_calls[0][1][0] + assert my_event.data[ATTR_HOST] == host + assert my_event.data[ATTR_AREA] == 7 + assert my_event.data[ATTR_PRESET] == 2 From 8fe11fec044ce7930a8dfd611cd354d4f9e5a24d Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 6 Aug 2020 19:43:42 +0100 Subject: [PATCH 017/862] Improve the OVO Energy integration (#38598) Co-authored-by: Martin Hjelmare --- .../components/ovo_energy/config_flow.py | 70 ++++++++----------- .../components/ovo_energy/manifest.json | 1 - .../components/ovo_energy/strings.json | 29 ++++---- .../ovo_energy/translations/en.json | 11 +-- 4 files changed, 51 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index e4d33865f57..ac3e8371123 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -9,58 +9,48 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import CONF_ACCOUNT_ID, DOMAIN +from .const import CONF_ACCOUNT_ID, DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) +USER_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) -@config_entries.HANDLERS.register(DOMAIN) -class OVOEnergyFlowHandler(ConfigFlow): + +class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a OVO Energy config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Initialize OVO Energy flow.""" - - async def _show_setup_form(self, errors=None): - """Show the setup form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} - ), - errors=errors or {}, - ) - async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" - if user_input is None: - return await self._show_setup_form() - errors = {} - - client = OVOEnergy() - - try: - if ( - await client.authenticate( - user_input.get(CONF_USERNAME), user_input.get(CONF_PASSWORD) + if user_input is not None: + client = OVOEnergy() + try: + authenticated = await client.authenticate( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] ) - is not True - ): - errors["base"] = "authorization_error" - return await self._show_setup_form(errors) - except aiohttp.ClientError: - errors["base"] = "connection_error" - return await self._show_setup_form(errors) + except aiohttp.ClientError: + errors["base"] = "connection_error" + else: + if authenticated: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() - return self.async_create_entry( - title=client.account_id, - data={ - CONF_USERNAME: user_input.get(CONF_USERNAME), - CONF_PASSWORD: user_input.get(CONF_PASSWORD), - CONF_ACCOUNT_ID: client.account_id, - }, + return self.async_create_entry( + title=client.account_id, + data={ + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_ACCOUNT_ID: client.account_id, + }, + ) + + errors["base"] = "authorization_error" + + return self.async_show_form( + step_id="user", data_schema=USER_SCHEMA, errors=errors ) diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 27a28863405..2da08d3339b 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -4,6 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ovo_energy", "requirements": ["ovoenergy==1.1.6"], - "dependencies": [], "codeowners": ["@timmo001"] } diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json index a98b0223644..0132f3582b6 100644 --- a/homeassistant/components/ovo_energy/strings.json +++ b/homeassistant/components/ovo_energy/strings.json @@ -1,18 +1,19 @@ { - "config": { - "error": { - "authorization_error": "Authorization error. Check your credentials.", - "connection_error": "Could not connect to OVO Energy." - }, - "step": { - "user": { - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "config": { + "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "authorization_error": "Authorization error. Check your credentials.", + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" }, - "description": "Set up an OVO Energy instance to access your energy usage.", - "title": "Add OVO Energy" - } + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "description": "Set up an OVO Energy instance to access your energy usage.", + "title": "Add OVO Energy Account" + } + } } - } } diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json index 64a3bfe80c3..0132f3582b6 100644 --- a/homeassistant/components/ovo_energy/translations/en.json +++ b/homeassistant/components/ovo_energy/translations/en.json @@ -1,18 +1,19 @@ { "config": { "error": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "authorization_error": "Authorization error. Check your credentials.", - "connection_error": "Could not connect to OVO Energy." + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" }, "step": { "user": { "data": { - "password": "Password", - "username": "Username" + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" }, "description": "Set up an OVO Energy instance to access your energy usage.", - "title": "Add OVO Energy" + "title": "Add OVO Energy Account" } } } -} \ No newline at end of file +} From 937d993a67d9e15861252405e7b5896ae1737fa0 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Thu, 6 Aug 2020 18:47:39 -0400 Subject: [PATCH 018/862] Expose video doorbell button state to HomeKit (#38617) --- homeassistant/components/homekit/const.py | 1 + homeassistant/components/homekit/type_cameras.py | 11 +++++++++++ tests/components/homekit/test_type_cameras.py | 13 +++++++++++++ 3 files changed, 25 insertions(+) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index e38b86a7032..d8eec057191 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -129,6 +129,7 @@ SERV_OUTLET = "Outlet" SERV_SECURITY_SYSTEM = "SecuritySystem" SERV_SMOKE_SENSOR = "SmokeSensor" SERV_SPEAKER = "Speaker" +SERV_STATELESS_PROGRAMMABLE_SWITCH = "StatelessProgrammableSwitch" SERV_SWITCH = "Switch" SERV_TELEVISION = "Television" SERV_TELEVISION_SPEAKER = "TelevisionSpeaker" diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 93b822f9e7a..91b13a93eca 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -54,6 +54,7 @@ from .const import ( SERV_DOORBELL, SERV_MOTION_SENSOR, SERV_SPEAKER, + SERV_STATELESS_PROGRAMMABLE_SWITCH, ) from .img_util import scale_jpeg_camera_image from .util import pid_is_alive @@ -211,6 +212,7 @@ class Camera(HomeAccessory, PyhapCamera): self._async_update_motion_state(state) self._char_doorbell_detected = None + self._char_doorbell_detected_switch = None self.linked_doorbell_sensor = self.config.get(CONF_LINKED_DOORBELL_SENSOR) if self.linked_doorbell_sensor: state = self.hass.states.get(self.linked_doorbell_sensor) @@ -220,6 +222,14 @@ class Camera(HomeAccessory, PyhapCamera): self._char_doorbell_detected = serv_doorbell.configure_char( CHAR_PROGRAMMABLE_SWITCH_EVENT, value=0, ) + serv_stateless_switch = self.add_preload_service( + SERV_STATELESS_PROGRAMMABLE_SWITCH + ) + self._char_doorbell_detected_switch = serv_stateless_switch.configure_char( + CHAR_PROGRAMMABLE_SWITCH_EVENT, + value=0, + valid_values={"SinglePress": DOORBELL_SINGLE_PRESS}, + ) serv_speaker = self.add_preload_service(SERV_SPEAKER) serv_speaker.configure_char(CHAR_MUTE, value=0) @@ -282,6 +292,7 @@ class Camera(HomeAccessory, PyhapCamera): if new_state.state == STATE_ON: self._char_doorbell_detected.set_value(DOORBELL_SINGLE_PRESS) + self._char_doorbell_detected_switch.set_value(DOORBELL_SINGLE_PRESS) _LOGGER.debug( "%s: Set linked doorbell %s sensor to %d", self.entity_id, diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 9e8faa34d38..118ce2d9934 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -21,6 +21,7 @@ from homeassistant.components.homekit.const import ( DEVICE_CLASS_OCCUPANCY, SERV_DOORBELL, SERV_MOTION_SENSOR, + SERV_STATELESS_PROGRAMMABLE_SWITCH, VIDEO_CODEC_COPY, VIDEO_CODEC_H264_OMX, ) @@ -653,18 +654,28 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): assert char.value == 0 + service2 = acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH) + assert service2 + char2 = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT) + assert char2 + + assert char2.value == 0 + hass.states.async_set( doorbell_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY} ) await hass.async_block_till_done() assert char.value == 0 + assert char2.value == 0 char.set_value(True) + char2.set_value(True) hass.states.async_set( doorbell_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY} ) await hass.async_block_till_done() assert char.value == 0 + assert char2.value == 0 # Ensure we do not throw when the linked # doorbell sensor is removed @@ -673,6 +684,7 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): await acc.run_handler() await hass.async_block_till_done() assert char.value == 0 + assert char2.value == 0 async def test_camera_with_a_missing_linked_doorbell_sensor(hass, run_driver, events): @@ -703,3 +715,4 @@ async def test_camera_with_a_missing_linked_doorbell_sensor(hass, run_driver, ev assert acc.category == 17 # Camera assert not acc.get_service(SERV_DOORBELL) + assert not acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH) From 297dd9bbc26372d55405ce5ceea694da09200170 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 6 Aug 2020 22:43:03 -0400 Subject: [PATCH 019/862] Add vizio service to update a device setting (#36739) * track all settings and add service to update a setting * sort setting types * reduce frequency of updates due to the increase in API calls per update * change dict call to a get in case audio settings aren't available unlikely to occur but less error prone * Update if statement to be more consistent * revert changes to track all settings and store in state machine * revert one more change * force setting_type and setting_name to lowercase to make it easier to understand how to make service call * make service calls even simpler by attempting to transform certain parameters as much as possible --- homeassistant/components/vizio/const.py | 14 ++++ .../components/vizio/media_player.py | 32 +++++++-- homeassistant/components/vizio/services.yaml | 15 ++++ tests/components/vizio/test_media_player.py | 68 +++++++++++++------ 4 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/vizio/services.yaml diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 72bd9b6b08a..6c8a46c358f 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -27,6 +27,18 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv +SERVICE_UPDATE_SETTING = "update_setting" + +ATTR_SETTING_TYPE = "setting_type" +ATTR_SETTING_NAME = "setting_name" +ATTR_NEW_VALUE = "new_value" + +UPDATE_SETTING_SCHEMA = { + vol.Required(ATTR_SETTING_TYPE): cv.string, + vol.Required(ATTR_SETTING_NAME): cv.string, + vol.Required(ATTR_NEW_VALUE): vol.Or(vol.Coerce(int), cv.string), +} + CONF_ADDITIONAL_CONFIGS = "additional_configs" CONF_APP_ID = "APP_ID" CONF_APPS = "apps" @@ -66,6 +78,8 @@ SUPPORTED_COMMANDS = { VIZIO_SOUND_MODE = "eq" VIZIO_AUDIO_SETTINGS = "audio" VIZIO_MUTE_ON = "on" +VIZIO_VOLUME = "volume" +VIZIO_MUTE = "mute" # Since Vizio component relies on device class, this dict will ensure that changes to # the values of DEVICE_CLASS_SPEAKER or DEVICE_CLASS_TV don't require changes to pyvizio. diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 4a93e7886ef..28201b51db7 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,7 +1,7 @@ """Vizio SmartCast Device support.""" from datetime import timedelta import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, Union from pyvizio import VizioAsync from pyvizio.api.apps import find_app_name @@ -24,6 +24,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -41,16 +42,20 @@ from .const import ( DEVICE_ID, DOMAIN, ICON, + SERVICE_UPDATE_SETTING, SUPPORTED_COMMANDS, + UPDATE_SETTING_SCHEMA, VIZIO_AUDIO_SETTINGS, VIZIO_DEVICE_CLASSES, + VIZIO_MUTE, VIZIO_MUTE_ON, VIZIO_SOUND_MODE, + VIZIO_VOLUME, ) _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=30) PARALLEL_UPDATES = 0 @@ -113,6 +118,10 @@ async def async_setup_entry( entity = VizioDevice(config_entry, device, name, device_class) async_add_entities([entity], update_before_add=True) + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_UPDATE_SETTING, UPDATE_SETTING_SCHEMA, "async_update_setting" + ) class VizioDevice(MediaPlayerEntity): @@ -203,10 +212,13 @@ class VizioDevice(MediaPlayerEntity): audio_settings = await self._device.get_all_settings( VIZIO_AUDIO_SETTINGS, log_api_exception=False ) + if audio_settings: - self._volume_level = float(audio_settings["volume"]) / self._max_volume - if "mute" in audio_settings: - self._is_volume_muted = audio_settings["mute"].lower() == VIZIO_MUTE_ON + self._volume_level = float(audio_settings[VIZIO_VOLUME]) / self._max_volume + if VIZIO_MUTE in audio_settings: + self._is_volume_muted = ( + audio_settings[VIZIO_MUTE].lower() == VIZIO_MUTE_ON + ) else: self._is_volume_muted = None @@ -274,6 +286,16 @@ class VizioDevice(MediaPlayerEntity): self._volume_step = config_entry.options[CONF_VOLUME_STEP] self._conf_apps.update(config_entry.options.get(CONF_APPS, {})) + async def async_update_setting( + self, setting_type: str, setting_name: str, new_value: Union[int, str] + ) -> None: + """Update a setting when update_setting service is called.""" + await self._device.set_setting( + setting_type.lower().replace(" ", "_"), + setting_name.lower().replace(" ", "_"), + new_value, + ) + async def async_added_to_hass(self): """Register callbacks when entity is added.""" # Register callback for when config entry is updated. diff --git a/homeassistant/components/vizio/services.yaml b/homeassistant/components/vizio/services.yaml new file mode 100644 index 00000000000..c652b622de0 --- /dev/null +++ b/homeassistant/components/vizio/services.yaml @@ -0,0 +1,15 @@ +update_setting: + description: Update the value of a setting on a particular Vizio media player device. + fields: + entity_id: + description: Name of an entity to send command. + example: "media_player.vizio_smartcast" + setting_type: + description: The type of setting to be changed. Available types are listed in the `setting_types` property. + example: "audio" + setting_name: + description: The name of the setting to be changed. Available settings for a given setting_type are listed in the `_settings` property. + example: "eq" + new_value: + description: The new value for the setting + example: "Music" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 7a2ff1d1c7a..a4ef7c1a1ac 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -41,6 +41,7 @@ from homeassistant.components.vizio.const import ( CONF_APPS, CONF_VOLUME_STEP, DOMAIN, + SERVICE_UPDATE_SETTING, VIZIO_SCHEMA, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -174,13 +175,14 @@ async def _test_setup_speaker( unique_id=UNIQUE_ID, ) + audio_settings = { + "volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_SPEAKER] / 2), + "mute": "Off", + "eq": CURRENT_EQ, + } + async with _cm_for_test_setup_without_apps( - { - "volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_SPEAKER] / 2), - "mute": "Off", - "eq": CURRENT_EQ, - }, - vizio_power_state, + audio_settings, vizio_power_state, ): with patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", @@ -248,6 +250,7 @@ async def _test_setup_failure(hass: HomeAssistantType, config: str) -> None: async def _test_service( hass: HomeAssistantType, + domain: str, vizio_func_name: str, ha_service_name: str, additional_service_data: Optional[Dict[str, Any]], @@ -263,7 +266,7 @@ async def _test_service( f"homeassistant.components.vizio.media_player.VizioAsync.{vizio_func_name}" ) as service_call: await hass.services.async_call( - MP_DOMAIN, ha_service_name, service_data=service_data, blocking=True, + domain, ha_service_name, service_data=service_data, blocking=True, ) assert service_call.called @@ -347,29 +350,49 @@ async def test_services( """Test all Vizio media player entity services.""" await _test_setup_tv(hass, True) - await _test_service(hass, "pow_on", SERVICE_TURN_ON, None) - await _test_service(hass, "pow_off", SERVICE_TURN_OFF, None) + await _test_service(hass, MP_DOMAIN, "pow_on", SERVICE_TURN_ON, None) + await _test_service(hass, MP_DOMAIN, "pow_off", SERVICE_TURN_OFF, None) await _test_service( - hass, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} + hass, MP_DOMAIN, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} ) await _test_service( - hass, "mute_off", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False} + hass, + MP_DOMAIN, + "mute_off", + SERVICE_VOLUME_MUTE, + {ATTR_MEDIA_VOLUME_MUTED: False}, ) await _test_service( - hass, "set_input", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "USB"}, "USB" + hass, + MP_DOMAIN, + "set_input", + SERVICE_SELECT_SOURCE, + {ATTR_INPUT_SOURCE: "USB"}, + "USB", ) - await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, None) - await _test_service(hass, "vol_down", SERVICE_VOLUME_DOWN, None) + await _test_service(hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None) + await _test_service(hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_DOWN, None) await _test_service( - hass, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} + hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} ) await _test_service( - hass, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} + hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} ) - await _test_service(hass, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None) - await _test_service(hass, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None) + await _test_service(hass, MP_DOMAIN, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None) + await _test_service(hass, MP_DOMAIN, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None) await _test_service( - hass, "set_setting", SERVICE_SELECT_SOUND_MODE, {ATTR_SOUND_MODE: "Music"} + hass, + MP_DOMAIN, + "set_setting", + SERVICE_SELECT_SOUND_MODE, + {ATTR_SOUND_MODE: "Music"}, + ) + await _test_service( + hass, + DOMAIN, + "set_setting", + SERVICE_UPDATE_SETTING, + {"setting_type": "Audio", "setting_name": "EQ", "new_value": "Music"}, ) @@ -389,7 +412,9 @@ async def test_options_update( entry=config_entry, options=new_options, ) assert config_entry.options == updated_options - await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, None, num=VOLUME_STEP) + await _test_service( + hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None, num=VOLUME_STEP + ) async def _test_update_availability_switch( @@ -474,6 +499,7 @@ async def test_setup_with_apps( await _test_service( hass, + MP_DOMAIN, "launch_app", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: CURRENT_APP}, @@ -550,6 +576,7 @@ async def test_setup_with_apps_additional_apps_config( await _test_service( hass, + MP_DOMAIN, "launch_app", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "Netflix"}, @@ -557,6 +584,7 @@ async def test_setup_with_apps_additional_apps_config( ) await _test_service( hass, + MP_DOMAIN, "launch_app_config", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: CURRENT_APP}, From 5cc7605d2091065a64706e1ce6f14922d99699b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Aug 2020 22:26:43 -0500 Subject: [PATCH 020/862] Ensure homekit pairing barcode is usable on dark themes (#38609) --- homeassistant/components/homekit/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 201a0529f82..2199371c00d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -354,7 +354,7 @@ def show_setup_message(hass, entry_id, bridge_name, pincode, uri): buffer = io.BytesIO() url = pyqrcode.create(uri) - url.svg(buffer, scale=5) + url.svg(buffer, scale=5, module_color="#000", background="#FFF") pairing_secret = secrets.token_hex(32) hass.data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR] = buffer.getvalue() From 6d0d5548e52494e870091ad13e48e02945d669a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Aug 2020 08:16:46 +0200 Subject: [PATCH 021/862] Do not report google states if nothing to report (#38608) --- homeassistant/components/google_assistant/report_state.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index ad944362721..6fc9de53e4a 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -67,6 +67,9 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig except SmartHomeError: continue + if not entities: + return + await google_config.async_report_state_all({"devices": {"states": entities}}) async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) From 881b6a831d591b9ebabbb9697b8d76083a59cb4c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Aug 2020 08:17:00 +0200 Subject: [PATCH 022/862] Handle unavailable input_select in Google Assistant (#38611) --- .../components/google_assistant/trait.py | 77 ++++++++++--------- .../google_assistant/test_smart_home.py | 2 - .../components/google_assistant/test_trait.py | 5 ++ 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 9afdff4045f..6ff19aedeb4 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1285,46 +1285,49 @@ class ModesTrait(_Trait): return features & media_player.SUPPORT_SELECT_SOUND_MODE + def _generate(self, name, settings): + """Generate a list of modes.""" + mode = { + "name": name, + "name_values": [ + {"name_synonym": self.SYNONYMS.get(name, [name]), "lang": "en"} + ], + "settings": [], + "ordered": False, + } + for setting in settings: + mode["settings"].append( + { + "setting_name": setting, + "setting_values": [ + { + "setting_synonym": self.SYNONYMS.get(setting, [setting]), + "lang": "en", + } + ], + } + ) + return mode + def sync_attributes(self): """Return mode attributes for a sync request.""" - - def _generate(name, settings): - mode = { - "name": name, - "name_values": [ - {"name_synonym": self.SYNONYMS.get(name, [name]), "lang": "en"} - ], - "settings": [], - "ordered": False, - } - for setting in settings: - mode["settings"].append( - { - "setting_name": setting, - "setting_values": [ - { - "setting_synonym": self.SYNONYMS.get( - setting, [setting] - ), - "lang": "en", - } - ], - } - ) - return mode - - attrs = self.state.attributes modes = [] - if self.state.domain == media_player.DOMAIN: - if media_player.ATTR_SOUND_MODE_LIST in attrs: - modes.append( - _generate("sound mode", attrs[media_player.ATTR_SOUND_MODE_LIST]) - ) - elif self.state.domain == input_select.DOMAIN: - modes.append(_generate("option", attrs[input_select.ATTR_OPTIONS])) - elif self.state.domain == humidifier.DOMAIN: - if humidifier.ATTR_AVAILABLE_MODES in attrs: - modes.append(_generate("mode", attrs[humidifier.ATTR_AVAILABLE_MODES])) + + for domain, attr, name in ( + (media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"), + (input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"), + (humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"), + ): + if self.state.domain != domain: + continue + + items = self.state.attributes.get(attr) + + if items is not None: + modes.append(self._generate(name, items)) + + # Shortcut since all domains are currently unique + break payload = {"availableModes": modes} diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index e9795a9320f..6cd99d1fdd1 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -337,8 +337,6 @@ async def test_execute(hass): const.SOURCE_CLOUD, ) - print(result) - assert result == { "requestId": REQ_ID, "payload": { diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 0ca53d256a4..54a8acf1a65 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1515,6 +1515,11 @@ async def test_modes_input_select(hass): assert helpers.get_google_type(input_select.DOMAIN, None) is not None assert trait.ModesTrait.supported(input_select.DOMAIN, None, None) + trt = trait.ModesTrait( + hass, State("input_select.bla", "unavailable"), BASIC_CONFIG, + ) + assert trt.sync_attributes() == {"availableModes": []} + trt = trait.ModesTrait( hass, State( From 72a625104280ca1a9449071ad8fcd080bc57d856 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 7 Aug 2020 08:36:38 +0200 Subject: [PATCH 023/862] V2 timeout for async_add_entities (#38601) Co-authored-by: Martin Hjelmare Co-authored-by: J. Nick Koston Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/entity_platform.py | 15 +++++++++- tests/helpers/test_entity_platform.py | 38 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 7a581dbd19e..6d9a1275b06 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -23,6 +23,9 @@ if TYPE_CHECKING: SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 +SLOW_ADD_ENTITY_MAX_WAIT = 10 # Per Entity +SLOW_ADD_MIN_TIMEOUT = 60 + PLATFORM_NOT_READY_RETRIES = 10 DATA_ENTITY_PLATFORM = "entity_platform" PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds @@ -292,7 +295,17 @@ class EntityPlatform: if not tasks: return - await asyncio.gather(*tasks) + timeout = max(SLOW_ADD_ENTITY_MAX_WAIT * len(tasks), SLOW_ADD_MIN_TIMEOUT) + try: + async with self.hass.timeout.async_timeout(timeout, self.domain): + await asyncio.gather(*tasks) + except asyncio.TimeoutError: + self.logger.warning( + "Timed out adding entities for domain %s with platform %s after %ds", + self.domain, + self.platform_name, + timeout, + ) if self._async_unsub_polling is not None or not any( entity.should_poll for entity in self.entities.values() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 5912eb42b03..6d03b087151 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -931,3 +931,41 @@ async def test_invalid_entity_id(hass): await platform.async_add_entities([entity]) assert entity.hass is None assert entity.platform is None + + +class MockBlockingEntity(MockEntity): + """Class to mock an entity that will block adding entities.""" + + async def async_added_to_hass(self): + """Block for a long time.""" + await asyncio.sleep(1000) + + +async def test_setup_entry_with_entities_that_block_forever(hass, caplog): + """Test we cancel adding entities when we reach the timeout.""" + registry = mock_registry(hass) + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities([MockBlockingEntity(name="test1", unique_id="unique")]) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry(entry_id="super-mock-id") + mock_entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + with patch.object(entity_platform, "SLOW_ADD_ENTITY_MAX_WAIT", 0.01), patch.object( + entity_platform, "SLOW_ADD_MIN_TIMEOUT", 0.01 + ): + assert await mock_entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + full_name = f"{mock_entity_platform.domain}.{config_entry.domain}" + assert full_name in hass.config.components + assert len(hass.states.async_entity_ids()) == 0 + assert len(registry.entities) == 1 + assert "Timed out adding entities" in caplog.text + assert "test_domain.test1" in caplog.text + assert "test_domain" in caplog.text + assert "test" in caplog.text From fa41a7c6e792020e6651e675500f0c99b6017f97 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 7 Aug 2020 08:37:10 +0200 Subject: [PATCH 024/862] Bump OpenCV 4.3.0 and Numpy 1.19.1 (#38616) --- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 5d880888ef5..1f862bb1bbf 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,6 +3,6 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.19.0", "pyiqvia==0.2.1"], + "requirements": ["numpy==1.19.1", "pyiqvia==0.2.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index ed8fd9c662c..1fb7096d5fa 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,6 +2,6 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.19.0", "opencv-python-headless==4.2.0.32"], + "requirements": ["numpy==1.19.1", "opencv-python-headless==4.3.0.36"], "codeowners": [] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index b74633d36d4..b7d0361d087 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.19.0", + "numpy==1.19.1", "protobuf==3.6.1", "pillow==7.1.2" ], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index dabeabd2757..a43c2bb0cce 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.19.0"], + "requirements": ["numpy==1.19.1"], "codeowners": [], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index eb468e2cdf8..304078913e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -981,7 +981,7 @@ numato-gpio==0.8.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.19.0 +numpy==1.19.1 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -1002,7 +1002,7 @@ onvif-zeep-async==0.4.0 open-garage==0.1.4 # homeassistant.components.opencv -# opencv-python-headless==4.2.0.32 +# opencv-python-headless==4.3.0.36 # homeassistant.components.openerz openerz-api==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 597742a0d6d..236677c0856 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -461,7 +461,7 @@ numato-gpio==0.8.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.19.0 +numpy==1.19.1 # homeassistant.components.google oauth2client==4.0.0 From 7e34c2582f564ac7fbee4db3383e6c2ab3e9cbd1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Aug 2020 01:40:28 -0500 Subject: [PATCH 025/862] Ensure doorbird does not block startup (#38619) --- homeassistant/components/doorbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 58311fa65e4..23495a22bf8 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -2,7 +2,7 @@ "domain": "doorbird", "name": "DoorBird", "documentation": "https://www.home-assistant.io/integrations/doorbird", - "requirements": ["doorbirdpy==2.0.8"], + "requirements": ["doorbirdpy==2.1.0"], "dependencies": ["http"], "zeroconf": ["_axis-video._tcp.local."], "codeowners": ["@oblogic7", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 304078913e5..fea3c46d4a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -491,7 +491,7 @@ distro==1.5.0 dlipower==0.7.165 # homeassistant.components.doorbird -doorbirdpy==2.0.8 +doorbirdpy==2.1.0 # homeassistant.components.dovado dovado==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 236677c0856..c160661b4d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -252,7 +252,7 @@ directv==0.3.0 distro==1.5.0 # homeassistant.components.doorbird -doorbirdpy==2.0.8 +doorbirdpy==2.1.0 # homeassistant.components.dsmr dsmr_parser==0.18 From 3546a82cfb8be8da68e35fe65e78ebd0914495e3 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 7 Aug 2020 02:56:28 -0400 Subject: [PATCH 026/862] Upgrade to TensorFlow 2 (#38384) Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- azure-pipelines-wheels.yml | 1 + .../components/tensorflow/image_processing.py | 139 ++++++++++++------ .../components/tensorflow/manifest.json | 7 +- pylintrc | 2 +- requirements_all.txt | 13 +- script/gen_requirements_all.py | 1 + 6 files changed, 115 insertions(+), 48 deletions(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index c8943595429..ebe704f12e2 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -89,5 +89,6 @@ jobs: sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} sed -i "s|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} + sed -i "s|# tf-models-official|tf-models-official|g" ${requirement_file} done displayName: 'Prepare requirements files for Home Assistant wheels' diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index f4eb5342c46..d6d20c63f56 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -3,9 +3,11 @@ import io import logging import os import sys +import time from PIL import Image, ImageDraw, UnidentifiedImageError import numpy as np +import tensorflow as tf import voluptuous as vol from homeassistant.components.image_processing import ( @@ -16,16 +18,21 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, ) +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.util.pil import draw_box +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + +DOMAIN = "tensorflow" _LOGGER = logging.getLogger(__name__) ATTR_MATCHES = "matches" ATTR_SUMMARY = "summary" ATTR_TOTAL_MATCHES = "total_matches" +ATTR_PROCESS_TIME = "process_time" CONF_AREA = "area" CONF_BOTTOM = "bottom" @@ -34,6 +41,7 @@ CONF_CATEGORY = "category" CONF_FILE_OUT = "file_out" CONF_GRAPH = "graph" CONF_LABELS = "labels" +CONF_LABEL_OFFSET = "label_offset" CONF_LEFT = "left" CONF_MODEL = "model" CONF_MODEL_DIR = "model_dir" @@ -58,12 +66,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]), vol.Required(CONF_MODEL): vol.Schema( { - vol.Required(CONF_GRAPH): cv.isfile, + vol.Required(CONF_GRAPH): cv.isdir, vol.Optional(CONF_AREA): AREA_SCHEMA, vol.Optional(CONF_CATEGORIES, default=[]): vol.All( cv.ensure_list, [vol.Any(cv.string, CATEGORY_SCHEMA)] ), vol.Optional(CONF_LABELS): cv.isfile, + vol.Optional(CONF_LABEL_OFFSET, default=1): int, vol.Optional(CONF_MODEL_DIR): cv.isdir, } ), @@ -71,17 +80,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +def get_model_detection_function(model): + """Get a tf.function for detection.""" + + @tf.function + def detect_fn(image): + """Detect objects in image.""" + + image, shapes = model.preprocess(image) + prediction_dict = model.predict(image, shapes) + detections = model.postprocess(prediction_dict, shapes) + + return detections + + return detect_fn + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the TensorFlow image processing platform.""" - model_config = config.get(CONF_MODEL) + model_config = config[CONF_MODEL] model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow") labels = model_config.get(CONF_LABELS) or hass.config.path( "tensorflow", "object_detection", "data", "mscoco_label_map.pbtxt" ) + checkpoint = os.path.join(model_config[CONF_GRAPH], "checkpoint") + pipeline_config = os.path.join(model_config[CONF_GRAPH], "pipeline.config") # Make sure locations exist - if not os.path.isdir(model_dir) or not os.path.exists(labels): - _LOGGER.error("Unable to locate tensorflow models or label map") + if ( + not os.path.isdir(model_dir) + or not os.path.isdir(checkpoint) + or not os.path.exists(pipeline_config) + or not os.path.exists(labels) + ): + _LOGGER.error("Unable to locate tensorflow model or label map") return # append custom model path to sys.path @@ -89,18 +121,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: # Verify that the TensorFlow Object Detection API is pre-installed - os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" # These imports shouldn't be moved to the top, because they depend on code from the model_dir. # (The model_dir is created during the manual setup process. See integration docs.) - import tensorflow as tf # pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel - from object_detection.utils import label_map_util + from object_detection.utils import config_util, label_map_util + from object_detection.builders import model_builder except ImportError: _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " "for your system following instructions here: " - "https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/installation.md" + "https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/tf2.md#installation" ) return @@ -113,22 +144,45 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "PIL at reduced resolution" ) - # Set up Tensorflow graph, session, and label map to pass to processor - # pylint: disable=no-member - detection_graph = tf.Graph() - with detection_graph.as_default(): - od_graph_def = tf.GraphDef() - with tf.gfile.GFile(model_config.get(CONF_GRAPH), "rb") as fid: - serialized_graph = fid.read() - od_graph_def.ParseFromString(serialized_graph) - tf.import_graph_def(od_graph_def, name="") + hass.data[DOMAIN] = {CONF_MODEL: None} - session = tf.Session(graph=detection_graph) - label_map = label_map_util.load_labelmap(labels) - categories = label_map_util.convert_label_map_to_categories( - label_map, max_num_classes=90, use_display_name=True + def tensorflow_hass_start(_event): + """Set up TensorFlow model on hass start.""" + start = time.perf_counter() + + # Load pipeline config and build a detection model + pipeline_configs = config_util.get_configs_from_pipeline_file(pipeline_config) + detection_model = model_builder.build( + model_config=pipeline_configs["model"], is_training=False + ) + + # Restore checkpoint + ckpt = tf.compat.v2.train.Checkpoint(model=detection_model) + ckpt.restore(os.path.join(checkpoint, "ckpt-0")).expect_partial() + + _LOGGER.debug( + "Model checkpoint restore took %d seconds", time.perf_counter() - start + ) + + model = get_model_detection_function(detection_model) + + # Preload model cache with empty image tensor + inp = np.zeros([2160, 3840, 3], dtype=np.uint8) + # The input needs to be a tensor, convert it using `tf.convert_to_tensor`. + input_tensor = tf.convert_to_tensor(inp, dtype=tf.float32) + # The model expects a batch of images, so add an axis with `tf.newaxis`. + input_tensor = input_tensor[tf.newaxis, ...] + # Run inference + model(input_tensor) + + _LOGGER.debug("Model load took %d seconds", time.perf_counter() - start) + hass.data[DOMAIN][CONF_MODEL] = model + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, tensorflow_hass_start) + + category_index = label_map_util.create_category_index_from_labelmap( + labels, use_display_name=True ) - category_index = label_map_util.create_category_index(categories) entities = [] @@ -138,8 +192,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass, camera[CONF_ENTITY_ID], camera.get(CONF_NAME), - session, - detection_graph, category_index, config, ) @@ -152,14 +204,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): """Representation of an TensorFlow image processor.""" def __init__( - self, - hass, - camera_entity, - name, - session, - detection_graph, - category_index, - config, + self, hass, camera_entity, name, category_index, config, ): """Initialize the TensorFlow entity.""" model_config = config.get(CONF_MODEL) @@ -169,13 +214,12 @@ class TensorFlowImageProcessor(ImageProcessingEntity): self._name = name else: self._name = "TensorFlow {}".format(split_entity_id(camera_entity)[1]) - self._session = session - self._graph = detection_graph self._category_index = category_index self._min_confidence = config.get(CONF_CONFIDENCE) self._file_out = config.get(CONF_FILE_OUT) # handle categories and specific detection areas + self._label_id_offset = model_config.get(CONF_LABEL_OFFSET) categories = model_config.get(CONF_CATEGORIES) self._include_categories = [] self._category_areas = {} @@ -212,6 +256,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): self._matches = {} self._total_matches = 0 self._last_image = None + self._process_time = 0 @property def camera_entity(self): @@ -237,6 +282,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): category: len(values) for category, values in self._matches.items() }, ATTR_TOTAL_MATCHES: self._total_matches, + ATTR_PROCESS_TIME: self._process_time, } def _save_image(self, image, matches, paths): @@ -281,10 +327,16 @@ class TensorFlowImageProcessor(ImageProcessingEntity): def process_image(self, image): """Process the image.""" + model = self.hass.data[DOMAIN][CONF_MODEL] + if not model: + _LOGGER.debug("Model not yet ready.") + return + start = time.perf_counter() try: import cv2 # pylint: disable=import-error, import-outside-toplevel + # pylint: disable=no-member img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) inp = img[:, :, [2, 1, 0]] # BGR->RGB inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) @@ -303,15 +355,15 @@ class TensorFlowImageProcessor(ImageProcessingEntity): ) inp_expanded = np.expand_dims(inp, axis=0) - image_tensor = self._graph.get_tensor_by_name("image_tensor:0") - boxes = self._graph.get_tensor_by_name("detection_boxes:0") - scores = self._graph.get_tensor_by_name("detection_scores:0") - classes = self._graph.get_tensor_by_name("detection_classes:0") - boxes, scores, classes = self._session.run( - [boxes, scores, classes], feed_dict={image_tensor: inp_expanded} - ) - boxes, scores, classes = map(np.squeeze, [boxes, scores, classes]) - classes = classes.astype(int) + # The input needs to be a tensor, convert it using `tf.convert_to_tensor`. + input_tensor = tf.convert_to_tensor(inp_expanded, dtype=tf.float32) + + detections = model(input_tensor) + boxes = detections["detection_boxes"][0].numpy() + scores = detections["detection_scores"][0].numpy() + classes = ( + detections["detection_classes"][0].numpy() + self._label_id_offset + ).astype(int) matches = {} total_matches = 0 @@ -367,3 +419,4 @@ class TensorFlowImageProcessor(ImageProcessingEntity): self._matches = matches self._total_matches = total_matches + self._process_time = time.perf_counter() - start diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index b7d0361d087..fc87b5cdbff 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -3,9 +3,12 @@ "name": "TensorFlow", "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ - "tensorflow==1.13.2", + "tensorflow==2.2.0", + "tf-slim==1.1.0", + "tf-models-official==2.2.1", + "pycocotools==2.0.1", "numpy==1.19.1", - "protobuf==3.6.1", + "protobuf==3.12.2", "pillow==7.1.2" ], "codeowners": [] diff --git a/pylintrc b/pylintrc index df53c2f67a2..f2860026cd8 100644 --- a/pylintrc +++ b/pylintrc @@ -5,7 +5,7 @@ ignore=tests jobs=2 load-plugins=pylint_strict_informational persistent=no -extension-pkg-whitelist=ciso8601 +extension-pkg-whitelist=ciso8601,cv2 [BASIC] good-names=id,i,j,k,ex,Run,_,fp,T,ev diff --git a/requirements_all.txt b/requirements_all.txt index fea3c46d4a4..5a9a0ea64c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1120,7 +1120,7 @@ proliphix==0.4.1 prometheus_client==0.7.1 # homeassistant.components.tensorflow -protobuf==3.6.1 +protobuf==3.12.2 # homeassistant.components.proxmoxve proxmoxer==1.1.1 @@ -1261,6 +1261,9 @@ pychromecast==7.2.0 # homeassistant.components.cmus pycmus==0.1.1 +# homeassistant.components.tensorflow +pycocotools==2.0.1 + # homeassistant.components.comfoconnect pycomfoconnect==0.3 @@ -2098,7 +2101,7 @@ temescal==0.1 temperusb==1.5.3 # homeassistant.components.tensorflow -# tensorflow==1.13.2 +# tensorflow==2.2.0 # homeassistant.components.powerwall tesla-powerwall==0.2.12 @@ -2106,6 +2109,12 @@ tesla-powerwall==0.2.12 # homeassistant.components.tesla teslajsonpy==0.10.1 +# homeassistant.components.tensorflow +# tf-models-official==2.2.1 + +# homeassistant.components.tensorflow +tf-slim==1.1.0 + # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4625924da29..772b9af5034 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -41,6 +41,7 @@ COMMENT_REQUIREMENTS = ( "RPi.GPIO", "smbus-cffi", "tensorflow", + "tf-models-official", "VL53L1X2", ) From 6930aebea277cd9965d8478475932d151e5b6c38 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 7 Aug 2020 09:25:59 +0200 Subject: [PATCH 027/862] Switch Netatmo integration to dispatcher for internal communication (#38590) * Switch to dispatcher for internal communication * Fix method call * Update homeassistant/components/netatmo/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/netatmo/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/netatmo/climate.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/netatmo/climate.py Co-authored-by: Martin Hjelmare * Rename variables Co-authored-by: Martin Hjelmare --- homeassistant/components/netatmo/camera.py | 74 +++++-------- homeassistant/components/netatmo/climate.py | 101 +++++++++--------- .../components/netatmo/config_flow.py | 6 +- homeassistant/components/netatmo/const.py | 13 ++- .../components/netatmo/data_handler.py | 45 ++++---- homeassistant/components/netatmo/light.py | 63 +++++------ homeassistant/components/netatmo/webhook.py | 33 +++--- 7 files changed, 170 insertions(+), 165 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 8fbff3225dd..39f6839d331 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -5,14 +5,10 @@ import pyatmo import requests import voluptuous as vol -from homeassistant.components.camera import ( - DOMAIN as CAMERA_DOMAIN, - SUPPORT_STREAM, - Camera, -) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( ATTR_PERSON, @@ -21,10 +17,12 @@ from .const import ( DATA_HANDLER, DATA_PERSONS, DOMAIN, + EVENT_TYPE_OFF, + EVENT_TYPE_ON, MANUFACTURER, MODELS, - SERVICE_SETPERSONAWAY, - SERVICE_SETPERSONSHOME, + SERVICE_SET_PERSON_AWAY, + SERVICE_SET_PERSONS_HOME, SIGNAL_NAME, ) from .data_handler import CAMERA_DATA_CLASS_NAME @@ -34,20 +32,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_QUALITY = "high" -SCHEMA_SERVICE_SETPERSONSHOME = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_domain(CAMERA_DOMAIN), - vol.Required(ATTR_PERSONS): vol.All(cv.ensure_list, [cv.string]), - } -) - -SCHEMA_SERVICE_SETPERSONAWAY = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_domain(CAMERA_DOMAIN), - vol.Optional(ATTR_PERSON): cv.string, - } -) - async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera platform.""" @@ -108,22 +92,17 @@ async def async_setup_entry(hass, entry, async_add_entities): if data_handler.data[CAMERA_DATA_CLASS_NAME] is not None: platform.async_register_entity_service( - SERVICE_SETPERSONSHOME, - SCHEMA_SERVICE_SETPERSONSHOME, - "_service_setpersonshome", + SERVICE_SET_PERSONS_HOME, + {vol.Required(ATTR_PERSONS): vol.All(cv.ensure_list, [cv.string])}, + "_service_set_persons_home", ) platform.async_register_entity_service( - SERVICE_SETPERSONAWAY, - SCHEMA_SERVICE_SETPERSONAWAY, - "_service_setpersonaway", + SERVICE_SET_PERSON_AWAY, + {vol.Optional(ATTR_PERSON): cv.string}, + "_service_set_person_away", ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Netatmo camera platform.""" - return - - class NetatmoCamera(NetatmoBase, Camera): """Representation of a Netatmo camera.""" @@ -156,16 +135,19 @@ class NetatmoCamera(NetatmoBase, Camera): """Entity created.""" await super().async_added_to_hass() - self._listeners.append( - self.hass.bus.async_listen("netatmo_event", self.handle_event) - ) + for event_type in (EVENT_TYPE_OFF, EVENT_TYPE_ON): + self._listeners.append( + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{event_type}", + self.handle_event, + ) + ) - async def handle_event(self, event): + @callback + def handle_event(self, event): """Handle webhook events.""" - data = event.data["data"] - - if not data.get("event_type"): - return + data = event["data"] if not data.get("camera_id"): return @@ -278,7 +260,7 @@ class NetatmoCamera(NetatmoBase, Camera): self._is_local = camera.get("is_local") self.is_streaming = bool(self._status == "on") - def _service_setpersonshome(self, **kwargs): + def _service_set_persons_home(self, **kwargs): """Service to change current home schedule.""" persons = kwargs.get(ATTR_PERSONS) person_ids = [] @@ -288,9 +270,9 @@ class NetatmoCamera(NetatmoBase, Camera): person_ids.append(pid) self._data.set_persons_home(person_ids=person_ids, home_id=self._home_id) - _LOGGER.info("Set %s as at home", persons) + _LOGGER.debug("Set %s as at home", persons) - def _service_setpersonaway(self, **kwargs): + def _service_set_person_away(self, **kwargs): """Service to mark a person as away or set the home as empty.""" person = kwargs.get(ATTR_PERSON) person_id = None @@ -303,10 +285,10 @@ class NetatmoCamera(NetatmoBase, Camera): self._data.set_persons_away( person_id=person_id, home_id=self._home_id, ) - _LOGGER.info("Set %s as away", person) + _LOGGER.debug("Set %s as away", person) else: self._data.set_persons_away( person_id=person_id, home_id=self._home_id, ) - _LOGGER.info("Set home as empty") + _LOGGER.debug("Set home as empty") diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 459f005695b..acfcf4306fb 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -4,7 +4,7 @@ from typing import List, Optional import voluptuous as vol -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -19,7 +19,6 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ( ATTR_BATTERY_LEVEL, - ATTR_ENTITY_ID, ATTR_TEMPERATURE, PRECISION_HALVES, STATE_OFF, @@ -27,6 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( ATTR_HEATING_POWER_REQUEST, @@ -35,8 +35,11 @@ from .const import ( DATA_HOMES, DATA_SCHEDULES, DOMAIN, + EVENT_TYPE_CANCEL_SET_POINT, + EVENT_TYPE_SET_POINT, + EVENT_TYPE_THERM_MODE, MANUFACTURER, - SERVICE_SETSCHEDULE, + SERVICE_SET_SCHEDULE, SIGNAL_NAME, ) from .data_handler import HOMEDATA_DATA_CLASS_NAME, HOMESTATUS_DATA_CLASS_NAME @@ -95,13 +98,6 @@ DEFAULT_MAX_TEMP = 30 NA_THERM = "NATherm1" NA_VALVE = "NRV" -SCHEMA_SERVICE_SETSCHEDULE = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_domain(CLIMATE_DOMAIN), - vol.Required(ATTR_SCHEDULE_NAME): cv.string, - } -) - async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo energy platform.""" @@ -156,15 +152,12 @@ async def async_setup_entry(hass, entry, async_add_entities): if home_data is not None: platform.async_register_entity_service( - SERVICE_SETSCHEDULE, SCHEMA_SERVICE_SETSCHEDULE, "_service_setschedule", + SERVICE_SET_SCHEDULE, + {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, + "_service_set_schedule", ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Netatmo energy sensors.""" - return - - class NetatmoThermostat(NetatmoBase, ClimateEntity): """Representation a Netatmo thermostat.""" @@ -229,23 +222,29 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Entity created.""" await super().async_added_to_hass() - self._listeners.append( - self.hass.bus.async_listen("netatmo_event", self.handle_event) - ) + for event_type in ( + EVENT_TYPE_SET_POINT, + EVENT_TYPE_THERM_MODE, + EVENT_TYPE_CANCEL_SET_POINT, + ): + self._listeners.append( + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{event_type}", + self.handle_event, + ) + ) async def handle_event(self, event): """Handle webhook events.""" - data = event.data["data"] - - if not data.get("event_type"): - return + data = event["data"] if not data.get("home"): return home = data["home"] - if self._home_id == home["id"] and data["event_type"] == "therm_mode": - self._preset = NETATMO_MAP_PRESET[home["therm_mode"]] + if self._home_id == home["id"] and data["event_type"] == EVENT_TYPE_THERM_MODE: + self._preset = NETATMO_MAP_PRESET[home[EVENT_TYPE_THERM_MODE]] self._hvac_mode = HVAC_MAP_NETATMO[self._preset] if self._preset == PRESET_FROST_GUARD: self._target_temperature = self._hg_temperature @@ -260,7 +259,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return for room in home["rooms"]: - if data["event_type"] == "set_point": + if data["event_type"] == EVENT_TYPE_SET_POINT: if self._id == room["id"]: if room["therm_setpoint_mode"] == "off": self._hvac_mode = HVAC_MODE_OFF @@ -269,7 +268,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.async_write_ha_state() break - elif data["event_type"] == "cancel_set_point": + elif data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT: if self._id == room["id"]: self.async_update_callback() self.async_write_ha_state() @@ -411,10 +410,20 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): def async_update_callback(self): """Update the entity's state.""" self._home_status = self.data_handler.data[self._home_status_class] - self._room_status = self._home_status.rooms[self._id] - self._room_data = self._data.rooms[self._home_id][self._id] + self._room_status = self._home_status.rooms.get(self._id) + self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id) - roomstatus = {"roomID": self._room_status["id"]} + if not self._room_status or not self._room_data: + if self._connected: + _LOGGER.info( + "The thermostat in room %s seems to be out of reach", + self._device_name, + ) + + self._connected = False + return + + roomstatus = {"roomID": self._room_status.get("id", {})} if self._room_status.get("reachable"): roomstatus.update(self._build_room_status()) @@ -422,25 +431,17 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._hg_temperature = self._data.get_hg_temp(self._home_id) self._setpoint_duration = self._data.setpoint_duration[self._home_id] - try: - if self._model is None: - self._model = roomstatus["module_type"] - self._current_temperature = roomstatus["current_temperature"] - self._target_temperature = roomstatus["target_temperature"] - self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] - self._hvac_mode = HVAC_MAP_NETATMO[self._preset] - self._battery_level = roomstatus.get("battery_level") - self._connected = True + if "current_temperature" not in roomstatus: + return - except KeyError as err: - if self._connected: - _LOGGER.debug( - "The thermostat in room %s seems to be out of reach. (%s)", - self._device_name, - err, - ) - - self._connected = False + if self._model is None: + self._model = roomstatus["module_type"] + self._current_temperature = roomstatus["current_temperature"] + self._target_temperature = roomstatus["target_temperature"] + self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] + self._hvac_mode = HVAC_MAP_NETATMO[self._preset] + self._battery_level = roomstatus.get("battery_level") + self._connected = True self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] @@ -503,7 +504,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return {} - def _service_setschedule(self, **kwargs): + def _service_set_schedule(self, **kwargs): schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_id = None for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): @@ -515,7 +516,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id) - _LOGGER.info( + _LOGGER.debug( "Setting %s schedule to %s (%s)", self._home_id, kwargs.get(ATTR_SCHEDULE_NAME), diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index eedac3229c0..516f78e8019 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -69,7 +69,7 @@ class NetatmoFlowHandler( """Handle a flow start.""" await self.async_set_unique_id(DOMAIN) - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) @@ -108,7 +108,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): user_input={CONF_NEW_AREA: new_client} ) - return self._update_options() + return self._create_options_entry() weather_areas = list(self.options[CONF_WEATHER_AREAS]) @@ -183,7 +183,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="public_weather", data_schema=data_schema) - def _update_options(self): + def _create_options_entry(self): """Update config entry options.""" return self.async_create_entry( title="Netatmo Public Weather", data=self.options diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index c23b934c541..30e40d358d9 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -73,6 +73,13 @@ ATTR_SCHEDULE_NAME = "schedule_name" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) -SERVICE_SETSCHEDULE = "set_schedule" -SERVICE_SETPERSONSHOME = "set_persons_home" -SERVICE_SETPERSONAWAY = "set_person_away" +SERVICE_SET_SCHEDULE = "set_schedule" +SERVICE_SET_PERSONS_HOME = "set_persons_home" +SERVICE_SET_PERSON_AWAY = "set_person_away" + +EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" +EVENT_TYPE_LIGHT_MODE = "light_mode" +EVENT_TYPE_OFF = "off" +EVENT_TYPE_ON = "on" +EVENT_TYPE_SET_POINT = "set_point" +EVENT_TYPE_THERM_MODE = "therm_mode" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 414c89e13ec..8a299d0f072 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -11,6 +11,7 @@ import pyatmo from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval from .const import AUTH, DOMAIN, MANUFACTURER @@ -69,7 +70,9 @@ class NetatmoDataHandler: ) self.listeners.append( - self.hass.bus.async_listen("netatmo_event", self.handle_event) + async_dispatcher_connect( + self.hass, f"signal-{DOMAIN}-webhook-None", self.handle_event, + ) ) async def async_update(self, event_time): @@ -99,11 +102,11 @@ class NetatmoDataHandler: async def handle_event(self, event): """Handle webhook events.""" - if event.data["data"]["push_type"] == "webhook_activation": + if event["data"]["push_type"] == "webhook_activation": _LOGGER.info("%s webhook successfully registered", MANUFACTURER) self._webhook = True - elif event.data["data"]["push_type"] == "NACamera-connection": + elif event["data"]["push_type"] == "NACamera-connection": _LOGGER.debug("%s camera reconnected", MANUFACTURER) self._data_classes[CAMERA_DATA_CLASS_NAME][NEXT_SCAN] = time() @@ -126,27 +129,27 @@ class NetatmoDataHandler: self, data_class_name, data_class_entry, update_callback, **kwargs ): """Register data class.""" - if data_class_entry not in self._data_classes: - self._data_classes[data_class_entry] = { - "class": DATA_CLASSES[data_class_name], - "name": data_class_entry, - "interval": DEFAULT_INTERVALS[data_class_name], - NEXT_SCAN: time() + DEFAULT_INTERVALS[data_class_name], - "kwargs": kwargs, - "subscriptions": [update_callback], - } - - await self.async_fetch_data( - DATA_CLASSES[data_class_name], data_class_entry, **kwargs - ) - - self._queue.append(self._data_classes[data_class_entry]) - _LOGGER.debug("Data class %s added", data_class_entry) - - else: + if data_class_entry in self._data_classes: self._data_classes[data_class_entry]["subscriptions"].append( update_callback ) + return + + self._data_classes[data_class_entry] = { + "class": DATA_CLASSES[data_class_name], + "name": data_class_entry, + "interval": DEFAULT_INTERVALS[data_class_name], + NEXT_SCAN: time() + DEFAULT_INTERVALS[data_class_name], + "kwargs": kwargs, + "subscriptions": [update_callback], + } + + await self.async_fetch_data( + DATA_CLASSES[data_class_name], data_class_entry, **kwargs + ) + + self._queue.append(self._data_classes[data_class_entry]) + _LOGGER.debug("Data class %s added", data_class_entry) async def unregister_data_class(self, data_class_entry, update_callback): """Unregister data class.""" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 56cf7945402..dea56e54c09 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -6,8 +6,15 @@ import pyatmo from homeassistant.components.light import LightEntity from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_HANDLER, DOMAIN, MANUFACTURER, SIGNAL_NAME +from .const import ( + DATA_HANDLER, + DOMAIN, + EVENT_TYPE_LIGHT_MODE, + MANUFACTURER, + SIGNAL_NAME, +) from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler from .netatmo_entity_base import NetatmoBase @@ -31,42 +38,36 @@ async def async_setup_entry(hass, entry, async_add_entities): ) entities = [] + all_cameras = [] + + if CAMERA_DATA_CLASS_NAME not in data_handler.data: + raise PlatformNotReady + try: - all_cameras = [] for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values(): for camera in home.values(): all_cameras.append(camera) - for camera in all_cameras: - if camera["type"] == "NOC": - if not data_handler.webhook: - raise PlatformNotReady - - _LOGGER.debug( - "Adding camera light %s %s", camera["id"], camera["name"] - ) - entities.append( - NetatmoLight( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], - ) - ) - except pyatmo.NoDevice: _LOGGER.debug("No cameras found") + for camera in all_cameras: + if camera["type"] == "NOC": + if not data_handler.webhook: + raise PlatformNotReady + + _LOGGER.debug("Adding camera light %s %s", camera["id"], camera["name"]) + entities.append( + NetatmoLight( + data_handler, camera["id"], camera["type"], camera["home_id"], + ) + ) + return entities async_add_entities(await get_entities(), True) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Netatmo camera platform.""" - return - - class NetatmoLight(NetatmoBase, LightEntity): """Representation of a Netatmo Presence camera light.""" @@ -97,15 +98,17 @@ class NetatmoLight(NetatmoBase, LightEntity): await super().async_added_to_hass() self._listeners.append( - self.hass.bus.async_listen("netatmo_event", self.handle_event) + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{EVENT_TYPE_LIGHT_MODE}", + self.handle_event, + ) ) - async def handle_event(self, event): + @callback + def handle_event(self, event): """Handle webhook events.""" - data = event.data["data"] - - if not data.get("event_type"): - return + data = event["data"] if not data.get("camera_id"): return diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py index 7126551883a..582fce8985c 100644 --- a/homeassistant/components/netatmo/webhook.py +++ b/homeassistant/components/netatmo/webhook.py @@ -2,6 +2,7 @@ import logging from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( ATTR_EVENT_TYPE, @@ -36,10 +37,9 @@ async def handle_webhook(hass, webhook_id, request): event_type = data.get(ATTR_EVENT_TYPE) - if event_type in ["outdoor", "therm_mode"]: - hass.bus.async_fire( - event_type=NETATMO_EVENT, event_data={"type": event_type, "data": data} - ) + if event_type in EVENT_TYPE_MAP: + async_send_event(hass, event_type, data) + for event_data in data.get(EVENT_TYPE_MAP[event_type], []): async_evaluate_event(hass, event_data) @@ -61,13 +61,22 @@ def async_evaluate_event(hass, event_data): ) person_event_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) person_event_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) - hass.bus.async_fire( - event_type=NETATMO_EVENT, - event_data={"type": event_type, "data": person_event_data}, - ) + + async_send_event(hass, event_type, person_event_data) + else: _LOGGER.debug("%s: %s", event_type, event_data) - hass.bus.async_fire( - event_type=NETATMO_EVENT, - event_data={"type": event_type, "data": event_data}, - ) + async_send_event(hass, event_type, event_data) + + +@callback +def async_send_event(hass, event_type, data): + """Send events.""" + hass.bus.async_fire( + event_type=NETATMO_EVENT, event_data={"type": event_type, "data": data} + ) + async_dispatcher_send( + hass, + f"signal-{DOMAIN}-webhook-{event_type}", + {"type": event_type, "data": data}, + ) From 93e1f6176a8713a0ceb550cb2b9ce1742e0b1f10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 7 Aug 2020 08:40:50 +0000 Subject: [PATCH 028/862] Fix lint --- homeassistant/components/tensorflow/image_processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index d6d20c63f56..420c3403a11 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -7,7 +7,7 @@ import time from PIL import Image, ImageDraw, UnidentifiedImageError import numpy as np -import tensorflow as tf +import tensorflow as tf # pylint: disable=import-error import voluptuous as vol from homeassistant.components.image_processing import ( From fa956e3153992e38d781e1e5c1ece095e2dfa623 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Aug 2020 11:41:26 +0200 Subject: [PATCH 029/862] Bump version to 0.115.0dev0 (#38606) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cedfced2f7c..8b075d37950 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 114 +MINOR_VERSION = 115 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 94fd6cceefd02ea69113ac3f59b2c0762bbafa1f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Aug 2020 17:37:31 +0200 Subject: [PATCH 030/862] Remove tf-models-official from wheels builder (#38637) --- azure-pipelines-wheels.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index ebe704f12e2..c8943595429 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -89,6 +89,5 @@ jobs: sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} sed -i "s|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} - sed -i "s|# tf-models-official|tf-models-official|g" ${requirement_file} done displayName: 'Prepare requirements files for Home Assistant wheels' From 2d70df97760e1a58f9314783abfc4ef9cf7d4787 Mon Sep 17 00:00:00 2001 From: Ole-Martin Heggen Date: Fri, 7 Aug 2020 22:14:42 +0200 Subject: [PATCH 031/862] Fix url in seventeentrack delivered notification (#38646) --- homeassistant/components/seventeentrack/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 53b16944cb2..42b198d48d9 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -259,7 +259,7 @@ class SeventeenTrackPackageSensor(Entity): self._friendly_name if self._friendly_name else self._tracking_number ) message = NOTIFICATION_DELIVERED_MESSAGE.format( - self._tracking_number, identification + identification, self._tracking_number ) title = NOTIFICATION_DELIVERED_TITLE.format(identification) notification_id = NOTIFICATION_DELIVERED_TITLE.format(self._tracking_number) From 3e9f2b82462c2ee8877d6bd4e47cb5bf3fe97eaa Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Fri, 7 Aug 2020 14:18:31 -0600 Subject: [PATCH 032/862] Remove unused async_setup_platform from HLK-SW16 switch (#38648) --- homeassistant/components/hlk_sw16/switch.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/hlk_sw16/switch.py b/homeassistant/components/hlk_sw16/switch.py index 9bd10ea765d..d2d6578cd90 100644 --- a/homeassistant/components/hlk_sw16/switch.py +++ b/homeassistant/components/hlk_sw16/switch.py @@ -7,10 +7,6 @@ from .const import DOMAIN PARALLEL_UPDATES = 0 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the HLK-SW16 switches.""" - - def devices_from_entities(hass, entry): """Parse configuration and add HLK-SW16 switch devices.""" device_client = hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] From 353817337edc9329f6b127dd82ae4d3cd42d455d Mon Sep 17 00:00:00 2001 From: Vaarlion <59558433+Vaarlion@users.noreply.github.com> Date: Fri, 7 Aug 2020 22:28:49 +0200 Subject: [PATCH 033/862] Automatically switch mpd between resume and start playing on media_play (#37854) * Automatically switch between resume and start playing * Fix Black issue Weirdly when i run it i had an error `1544 files left unchanged, 3313 files failed to reformat.` I didn't watch the commit check output afterward. --- homeassistant/components/mpd/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index ba9b2f73d3c..201e8ed64e1 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -307,7 +307,10 @@ class MpdDevice(MediaPlayerEntity): def media_play(self): """Service to send the MPD the command for play/pause.""" - self._client.pause(0) + if self._status["state"] == "pause": + self._client.pause(0) + else: + self._client.play() def media_pause(self): """Service to send the MPD the command for play/pause.""" From 4cceb4ad0a50e6d14b2ba34764cae8cb5a22b270 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 7 Aug 2020 18:01:55 -0600 Subject: [PATCH 034/862] Bump regenmaschine to 2.1.0 (#38649) --- homeassistant/components/rainmachine/__init__.py | 2 +- homeassistant/components/rainmachine/config_flow.py | 6 +++--- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rainmachine/test_config_flow.py | 7 +++---- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 2e32d0ed43d..239878d0219 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -133,7 +133,7 @@ async def async_setup_entry(hass, config_entry): _verify_domain_control = verify_domain_control(hass, DOMAIN) websession = aiohttp_client.async_get_clientsession(hass) - client = Client(websession) + client = Client(session=websession) try: await client.load_local( diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index dc1ee16d05f..d0513ac89fb 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the RainMachine component.""" -from regenmaschine import login +from regenmaschine import Client from regenmaschine.errors import RainMachineError import voluptuous as vol @@ -59,12 +59,12 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() websession = aiohttp_client.async_get_clientsession(self.hass) + client = Client(session=websession) try: - await login( + await client.load_local( user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], - websession, port=user_input[CONF_PORT], ssl=user_input.get(CONF_SSL, True), ) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index aed0f030c25..07321801381 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,6 +3,6 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==1.5.1"], + "requirements": ["regenmaschine==2.1.0"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5a9a0ea64c9..074acb862bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1876,7 +1876,7 @@ raspyrfm-client==1.2.8 recollect-waste==1.0.1 # homeassistant.components.rainmachine -regenmaschine==1.5.1 +regenmaschine==2.1.0 # homeassistant.components.python_script restrictedpython==5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c160661b4d4..4c65f8b240d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -842,7 +842,7 @@ pyzerproc==0.2.5 rachiopy==0.1.3 # homeassistant.components.rainmachine -regenmaschine==1.5.1 +regenmaschine==2.1.0 # homeassistant.components.python_script restrictedpython==5.0 diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 04dc67bdbe8..7b27bdf2f39 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -50,8 +50,7 @@ async def test_invalid_password(hass): flow.context = {"source": SOURCE_USER} with patch( - "homeassistant.components.rainmachine.config_flow.login", - side_effect=RainMachineError, + "regenmaschine.client.Client.load_local", side_effect=RainMachineError, ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {CONF_PASSWORD: "invalid_credentials"} @@ -84,7 +83,7 @@ async def test_step_import(hass): flow.context = {"source": SOURCE_USER} with patch( - "homeassistant.components.rainmachine.config_flow.login", return_value=True, + "regenmaschine.client.Client.load_local", return_value=True, ): result = await flow.async_step_import(import_config=conf) @@ -115,7 +114,7 @@ async def test_step_user(hass): flow.context = {"source": SOURCE_USER} with patch( - "homeassistant.components.rainmachine.config_flow.login", return_value=True, + "regenmaschine.client.Client.load_local", return_value=True, ): result = await flow.async_step_user(user_input=conf) From 50cd6be18d1c44ff6a624e34f20186a93e1ceb17 Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Fri, 7 Aug 2020 22:22:13 -0400 Subject: [PATCH 035/862] Raise error when unsupported (old) Bond firmware is detected (#38650) --- homeassistant/components/bond/config_flow.py | 6 +++++- homeassistant/components/bond/strings.json | 1 + .../components/bond/translations/en.json | 3 ++- tests/components/bond/test_config_flow.py | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index aa22dc628da..ca8965d4d43 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -39,7 +39,11 @@ async def _validate_input(data: Dict[str, Any]) -> str: raise InputValidationError("unknown") # Return unique ID from the hub to be stored in the config entry. - return version["bondid"] + bond_id = version.get("bondid") + if not bond_id: + raise InputValidationError("old_firmware") + + return bond_id class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/bond/strings.json b/homeassistant/components/bond/strings.json index ba59a61d58d..5ca2278a3e5 100644 --- a/homeassistant/components/bond/strings.json +++ b/homeassistant/components/bond/strings.json @@ -18,6 +18,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "old_firmware": "Unsupported old firmware on the Bond device - please upgrade before continuing", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/bond/translations/en.json b/homeassistant/components/bond/translations/en.json index 2e636bb8999..f141a450652 100644 --- a/homeassistant/components/bond/translations/en.json +++ b/homeassistant/components/bond/translations/en.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "old_firmware": "Unsupported old firmware on the Bond device - please upgrade before continuing", "unknown": "Unexpected error" }, "flow_title": "Bond: {bond_id} ({host})", @@ -24,4 +25,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index bd499b8ce61..8671cc4b601 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -79,6 +79,24 @@ async def test_user_form_cannot_connect(hass: core.HomeAssistant): assert result2["errors"] == {"base": "cannot_connect"} +async def test_user_form_old_firmware(hass: core.HomeAssistant): + """Test we handle unsupported old firmware.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch_bond_version( + return_value={"no_bond_id": "present"} + ), patch_bond_device_ids(): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "old_firmware"} + + async def test_user_form_unexpected_client_error(hass: core.HomeAssistant): """Test we handle unexpected client error gracefully.""" await _help_test_form_unexpected_error( From 0d5e2795093a51567d61cd84cd0158bfcef40b0e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 7 Aug 2020 22:29:54 -0400 Subject: [PATCH 036/862] Standardize Vizio update service schema (#38636) * update voluptuous schema to match standards and to handle data transformations * improve test --- homeassistant/components/vizio/const.py | 6 +++--- homeassistant/components/vizio/media_player.py | 4 +--- tests/components/vizio/test_media_player.py | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 6c8a46c358f..e0b60769e45 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -34,9 +34,9 @@ ATTR_SETTING_NAME = "setting_name" ATTR_NEW_VALUE = "new_value" UPDATE_SETTING_SCHEMA = { - vol.Required(ATTR_SETTING_TYPE): cv.string, - vol.Required(ATTR_SETTING_NAME): cv.string, - vol.Required(ATTR_NEW_VALUE): vol.Or(vol.Coerce(int), cv.string), + vol.Required(ATTR_SETTING_TYPE): vol.All(cv.string, vol.Lower, cv.slugify), + vol.Required(ATTR_SETTING_NAME): vol.All(cv.string, vol.Lower, cv.slugify), + vol.Required(ATTR_NEW_VALUE): vol.Any(vol.Coerce(int), cv.string), } CONF_ADDITIONAL_CONFIGS = "additional_configs" diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 28201b51db7..408e37f011b 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -291,9 +291,7 @@ class VizioDevice(MediaPlayerEntity): ) -> None: """Update a setting when update_setting service is called.""" await self._device.set_setting( - setting_type.lower().replace(" ", "_"), - setting_name.lower().replace(" ", "_"), - new_value, + setting_type, setting_name, new_value, ) async def async_added_to_hass(self): diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index a4ef7c1a1ac..c0d780ed7ba 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -387,12 +387,26 @@ async def test_services( SERVICE_SELECT_SOUND_MODE, {ATTR_SOUND_MODE: "Music"}, ) + # Test that the update_setting service does config validation/transformation correctly + await _test_service( + hass, + DOMAIN, + "set_setting", + SERVICE_UPDATE_SETTING, + {"setting_type": "Audio", "setting_name": "AV Delay", "new_value": "0"}, + "audio", + "av_delay", + 0, + ) await _test_service( hass, DOMAIN, "set_setting", SERVICE_UPDATE_SETTING, {"setting_type": "Audio", "setting_name": "EQ", "new_value": "Music"}, + "audio", + "eq", + "Music", ) From 94b6d09b518a49ca250af0491c6cae9f9cbd02ba Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 7 Aug 2020 20:16:28 -0700 Subject: [PATCH 037/862] Update Tesla to use DataUpdateCoordinator (#38306) * Update Tesla to use DataUpdateCoordinator * Update Tesla to use DataUpdateCoordinator * Fix linting errors * Apply suggestions from code review Co-authored-by: Chris Talkington * Address requested changes * Apply suggestions from code review Co-authored-by: Chris Talkington * Fix lint errors * Remove controller from hass.data Co-authored-by: Chris Talkington --- homeassistant/components/tesla/__init__.py | 129 ++++++++++++----- .../components/tesla/binary_sensor.py | 27 +--- homeassistant/components/tesla/climate.py | 29 +--- .../components/tesla/device_tracker.py | 54 +++---- homeassistant/components/tesla/lock.py | 24 +--- homeassistant/components/tesla/manifest.json | 2 +- homeassistant/components/tesla/sensor.py | 135 ++++++++++-------- homeassistant/components/tesla/switch.py | 100 +++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 247 insertions(+), 257 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 67ebe90669d..1dc6bce01de 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -1,8 +1,10 @@ """Support for Tesla cars.""" import asyncio from collections import defaultdict +from datetime import timedelta import logging +import async_timeout from teslajsonpy import Controller as TeslaAPI, TeslaException import voluptuous as vol @@ -17,8 +19,10 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify from .config_flow import ( @@ -116,7 +120,6 @@ async def async_setup(hass, base_config): async def async_setup_entry(hass, config_entry): """Set up Tesla as config entry.""" - hass.data.setdefault(DOMAIN, {}) config = config_entry.data websession = aiohttp_client.async_get_clientsession(hass) @@ -145,13 +148,22 @@ async def async_setup_entry(hass, config_entry): _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False _async_save_tokens(hass, config_entry, access_token, refresh_token) + coordinator = TeslaDataUpdateCoordinator( + hass, config_entry=config_entry, controller=controller + ) + # Fetch initial data so we have data when entities subscribe entry_data = hass.data[DOMAIN][config_entry.entry_id] = { - "controller": controller, + "coordinator": coordinator, "devices": defaultdict(list), DATA_LISTENER: [config_entry.add_update_listener(update_listener)], } _LOGGER.debug("Connected to the Tesla API") - all_devices = entry_data["controller"].get_homeassistant_components() + + await coordinator.async_refresh() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + all_devices = controller.get_homeassistant_components() if not all_devices: return False @@ -169,54 +181,87 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry) -> bool: """Unload a config entry.""" - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in TESLA_COMPONENTS - ] + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in TESLA_COMPONENTS + ] + ) ) for listener in hass.data[DOMAIN][config_entry.entry_id][DATA_LISTENER]: listener() username = config_entry.title - hass.data[DOMAIN].pop(config_entry.entry_id) - _LOGGER.debug("Unloaded entry for %s", username) - return True + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + _LOGGER.debug("Unloaded entry for %s", username) + return True + return False async def update_listener(hass, config_entry): """Update when config_entry options update.""" - controller = hass.data[DOMAIN][config_entry.entry_id]["controller"] + controller = hass.data[DOMAIN][config_entry.entry_id]["coordinator"].controller old_update_interval = controller.update_interval controller.update_interval = config_entry.options.get(CONF_SCAN_INTERVAL) - _LOGGER.debug( - "Changing scan_interval from %s to %s", - old_update_interval, - controller.update_interval, - ) + if old_update_interval != controller.update_interval: + _LOGGER.debug( + "Changing scan_interval from %s to %s", + old_update_interval, + controller.update_interval, + ) + + +class TeslaDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Tesla data.""" + + def __init__(self, hass, *, config_entry, controller): + """Initialize global Tesla data updater.""" + self.controller = controller + self.config_entry = config_entry + + update_interval = timedelta(seconds=MIN_SCAN_INTERVAL) + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=update_interval, + ) + + async def _async_update_data(self): + """Fetch data from API endpoint.""" + if self.controller.is_token_refreshed(): + (refresh_token, access_token) = self.controller.get_tokens() + _async_save_tokens( + self.hass, self.config_entry, access_token, refresh_token + ) + _LOGGER.debug("Saving new tokens in config_entry") + + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(30): + return await self.controller.update() + except TeslaException as err: + raise UpdateFailed(f"Error communicating with API: {err}") class TeslaDevice(Entity): """Representation of a Tesla device.""" - def __init__(self, tesla_device, controller, config_entry): + def __init__(self, tesla_device, coordinator): """Initialise the Tesla device.""" self.tesla_device = tesla_device - self.controller = controller - self.config_entry = config_entry - self._name = self.tesla_device.name - self.tesla_id = slugify(self.tesla_device.uniq_name) - self._attributes = {} - self._icon = ICONS.get(self.tesla_device.type) + self.coordinator = coordinator + self._attributes = self.tesla_device.attrs.copy() @property def name(self): """Return the name of the device.""" - return self._name + return self.tesla_device.name @property def unique_id(self) -> str: """Return a unique ID.""" - return self.tesla_id + return slugify(self.tesla_device.uniq_name) @property def icon(self): @@ -224,17 +269,22 @@ class TeslaDevice(Entity): if self.device_class: return None - return self._icon + return ICONS.get(self.tesla_device.type) @property def should_poll(self): - """Return the polling state.""" - return self.tesla_device.should_poll + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @property + def available(self): + """Return if entity is available.""" + return self.coordinator.last_update_success @property def device_state_attributes(self): """Return the state attributes of the device.""" - attr = self._attributes + attr = self._attributes.copy() if self.tesla_device.has_battery(): attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() attr[ATTR_BATTERY_CHARGING] = self.tesla_device.battery_charging() @@ -253,16 +303,21 @@ class TeslaDevice(Entity): async def async_added_to_hass(self): """Register state update callback.""" + self.async_on_remove(self.coordinator.async_add_listener(self.refresh)) async def async_will_remove_from_hass(self): """Prepare for unload.""" async def async_update(self): """Update the state of the device.""" - if self.controller.is_token_refreshed(): - (refresh_token, access_token) = self.controller.get_tokens() - _async_save_tokens( - self.hass, self.config_entry, access_token, refresh_token - ) - _LOGGER.debug("Saving new tokens in config_entry") - await self.tesla_device.async_update() + _LOGGER.debug("Updating state for: %s", self.name) + await self.coordinator.async_request_refresh() + self.refresh() + + def refresh(self) -> None: + """Refresh the state of the device. + + This assumes the coordinator has updated the controller. + """ + self.tesla_device.refresh() + self.schedule_update_ha_state() diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index c6b63d92bd2..01dc1aa44f0 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -13,9 +13,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities( [ TeslaBinarySensor( - device, - hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], - config_entry, + device, hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], ) for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ "binary_sensor" @@ -28,27 +26,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TeslaBinarySensor(TeslaDevice, BinarySensorEntity): """Implement an Tesla binary sensor for parking and charger.""" - def __init__(self, tesla_device, controller, config_entry): - """Initialise of a Tesla binary sensor.""" - super().__init__(tesla_device, controller, config_entry) - self._state = None - self._sensor_type = None - if tesla_device.sensor_type in DEVICE_CLASSES: - self._sensor_type = tesla_device.sensor_type - @property def device_class(self): """Return the class of this binary sensor.""" - return self._sensor_type + return ( + self.tesla_device.sensor_type + if self.tesla_device.sensor_type in DEVICE_CLASSES + else None + ) @property def is_on(self): """Return the state of the binary sensor.""" - return self._state - - async def async_update(self): - """Update the state of the device.""" - _LOGGER.debug("Updating sensor: %s", self._name) - await super().async_update() - self._state = self.tesla_device.get_value() - self._attributes = self.tesla_device.attrs + return self.tesla_device.get_value() diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 31269155d91..bfc2f721a4b 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -25,9 +25,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities( [ TeslaThermostat( - device, - hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], - config_entry, + device, hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], ) for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ "climate" @@ -40,12 +38,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TeslaThermostat(TeslaDevice, ClimateEntity): """Representation of a Tesla climate.""" - def __init__(self, tesla_device, controller, config_entry): - """Initialize the Tesla device.""" - super().__init__(tesla_device, controller, config_entry) - self._target_temperature = None - self._temperature = None - @property def supported_features(self): """Return the list of supported features.""" @@ -69,42 +61,33 @@ class TeslaThermostat(TeslaDevice, ClimateEntity): """ return SUPPORT_HVAC - async def async_update(self): - """Call by the Tesla device callback to update state.""" - _LOGGER.debug("Updating: %s", self._name) - await super().async_update() - self._target_temperature = self.tesla_device.get_goal_temp() - self._temperature = self.tesla_device.get_current_temp() - @property def temperature_unit(self): """Return the unit of measurement.""" - tesla_temp_units = self.tesla_device.measurement - - if tesla_temp_units == "F": + if self.tesla_device.measurement == "F": return TEMP_FAHRENHEIT return TEMP_CELSIUS @property def current_temperature(self): """Return the current temperature.""" - return self._temperature + return self.tesla_device.get_current_temp() @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._target_temperature + return self.tesla_device.get_goal_temp() async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature: - _LOGGER.debug("%s: Setting temperature to %s", self._name, temperature) + _LOGGER.debug("%s: Setting temperature to %s", self.name, temperature) await self.tesla_device.set_temperature(temperature) async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" - _LOGGER.debug("%s: Setting hvac mode to %s", self._name, hvac_mode) + _LOGGER.debug("%s: Setting hvac mode to %s", self.name, hvac_mode) if hvac_mode == HVAC_MODE_OFF: await self.tesla_device.set_status(False) elif hvac_mode == HVAC_MODE_HEAT_COOL: diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index 08e5d58ba6e..ce5ea5a2a8a 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -1,5 +1,6 @@ """Support for tracking Tesla cars.""" import logging +from typing import Optional from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity @@ -13,9 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" entities = [ TeslaDeviceEntity( - device, - hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], - config_entry, + device, hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], ) for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ "devices_tracker" @@ -27,44 +26,37 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TeslaDeviceEntity(TeslaDevice, TrackerEntity): """A class representing a Tesla device.""" - def __init__(self, tesla_device, controller, config_entry): + def __init__(self, tesla_device, coordinator): """Initialize the Tesla device scanner.""" - super().__init__(tesla_device, controller, config_entry) - self._latitude = None - self._longitude = None + super().__init__(tesla_device, coordinator) self._attributes = {"trackr_id": self.unique_id} - self._listener = None - - async def async_update(self): - """Update the device info.""" - _LOGGER.debug("Updating device position: %s", self.name) - await super().async_update() - location = self.tesla_device.get_location() - if location: - self._latitude = location["latitude"] - self._longitude = location["longitude"] - self._attributes = { - "trackr_id": self.unique_id, - "heading": location["heading"], - "speed": location["speed"], - } @property - def latitude(self) -> float: + def latitude(self) -> Optional[float]: """Return latitude value of the device.""" - return self._latitude + location = self.tesla_device.get_location() + return self.tesla_device.get_location().get("latitude") if location else None @property - def longitude(self) -> float: + def longitude(self) -> Optional[float]: """Return longitude value of the device.""" - return self._longitude - - @property - def should_poll(self): - """Return whether polling is needed.""" - return True + location = self.tesla_device.get_location() + return self.tesla_device.get_location().get("longitude") if location else None @property def source_type(self): """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = super().device_state_attributes.copy() + location = self.tesla_device.get_location() + if location: + self._attributes = { + "trackr_id": self.unique_id, + "heading": location["heading"], + "speed": location["speed"], + } + return attr diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 91833d777fd..9f6db402422 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -2,7 +2,6 @@ import logging from homeassistant.components.lock import LockEntity -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from . import DOMAIN as TESLA_DOMAIN, TeslaDevice @@ -13,9 +12,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" entities = [ TeslaLock( - device, - hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], - config_entry, + device, hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], ) for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["lock"] ] @@ -25,28 +22,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TeslaLock(TeslaDevice, LockEntity): """Representation of a Tesla door lock.""" - def __init__(self, tesla_device, controller, config_entry): - """Initialise of the lock.""" - self._state = None - super().__init__(tesla_device, controller, config_entry) - async def async_lock(self, **kwargs): """Send the lock command.""" - _LOGGER.debug("Locking doors for: %s", self._name) + _LOGGER.debug("Locking doors for: %s", self.name) await self.tesla_device.lock() async def async_unlock(self, **kwargs): """Send the unlock command.""" - _LOGGER.debug("Unlocking doors for: %s", self._name) + _LOGGER.debug("Unlocking doors for: %s", self.name) await self.tesla_device.unlock() @property def is_locked(self): """Get whether the lock is in locked state.""" - return self._state == STATE_LOCKED - - async def async_update(self): - """Update state of the lock.""" - _LOGGER.debug("Updating state for: %s", self._name) - await super().async_update() - self._state = STATE_LOCKED if self.tesla_device.is_locked() else STATE_UNLOCKED + if self.tesla_device.is_locked() is None: + return None + return self.tesla_device.is_locked() diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index fab844eb8eb..11435ad2394 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.10.1"], + "requirements": ["teslajsonpy==0.10.3"], "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 62bdebbb1f3..93cc03cd718 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -1,6 +1,8 @@ """Support for the Tesla sensors.""" import logging +from typing import Optional +from homeassistant.components.sensor import DEVICE_CLASSES from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_MILES, @@ -17,89 +19,96 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" - controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] + coordinator = hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"] entities = [] for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["sensor"]: if device.type == "temperature sensor": - entities.append(TeslaSensor(device, controller, config_entry, "inside")) - entities.append(TeslaSensor(device, controller, config_entry, "outside")) + entities.append(TeslaSensor(device, coordinator, "inside")) + entities.append(TeslaSensor(device, coordinator, "outside")) else: - entities.append(TeslaSensor(device, controller, config_entry)) + entities.append(TeslaSensor(device, coordinator)) async_add_entities(entities, True) class TeslaSensor(TeslaDevice, Entity): """Representation of Tesla sensors.""" - def __init__(self, tesla_device, controller, config_entry, sensor_type=None): + def __init__(self, tesla_device, coordinator, sensor_type=None): """Initialize of the sensor.""" - self.current_value = None - self.units = None - self.last_changed_time = None + super().__init__(tesla_device, coordinator) self.type = sensor_type - self._device_class = tesla_device.device_class - super().__init__(tesla_device, controller, config_entry) - if self.type: - self._name = f"{self.tesla_device.name} ({self.type})" + @property + def name(self) -> str: + """Return the name of the device.""" + return ( + self.tesla_device.name + if not self.type + else f"{self.tesla_device.name} ({self.type})" + ) @property def unique_id(self) -> str: """Return a unique ID.""" - if self.type: - return f"{self.tesla_id}_{self.type}" - return self.tesla_id + return ( + super().unique_id if not self.type else f"{super().unique_id}_{self.type}" + ) @property - def state(self): + def state(self) -> Optional[float]: """Return the state of the sensor.""" - return self.current_value - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return self.units - - @property - def device_class(self): - """Return the device_class of the device.""" - return self._device_class - - async def async_update(self): - """Update the state from the sensor.""" - _LOGGER.debug("Updating sensor: %s", self._name) - await super().async_update() - units = self.tesla_device.measurement - if self.tesla_device.type == "temperature sensor": if self.type == "outside": - self.current_value = self.tesla_device.get_outside_temp() - else: - self.current_value = self.tesla_device.get_inside_temp() - if units == "F": - self.units = TEMP_FAHRENHEIT - else: - self.units = TEMP_CELSIUS - elif self.tesla_device.type in ["range sensor", "mileage sensor"]: - self.current_value = self.tesla_device.get_value() + return self.tesla_device.get_outside_temp() + return self.tesla_device.get_inside_temp() + if self.tesla_device.type in ["range sensor", "mileage sensor"]: + units = self.tesla_device.measurement if units == "LENGTH_MILES": - self.units = LENGTH_MILES - else: - self.units = LENGTH_KILOMETERS - self.current_value = round( - convert(self.current_value, LENGTH_MILES, LENGTH_KILOMETERS), 2 - ) - elif self.tesla_device.type == "charging rate sensor": - self.current_value = self.tesla_device.charging_rate - self.units = units - self._attributes = { - "time_left": self.tesla_device.time_left, - "added_range": self.tesla_device.added_range, - "charge_energy_added": self.tesla_device.charge_energy_added, - "charge_current_request": self.tesla_device.charge_current_request, - "charger_actual_current": self.tesla_device.charger_actual_current, - "charger_voltage": self.tesla_device.charger_voltage, - } - else: - self.current_value = self.tesla_device.get_value() - self.units = units + return self.tesla_device.get_value() + return round( + convert(self.tesla_device.get_value(), LENGTH_MILES, LENGTH_KILOMETERS), + 2, + ) + if self.tesla_device.type == "charging rate sensor": + return self.tesla_device.charging_rate + return self.tesla_device.get_value() + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit_of_measurement of the device.""" + units = self.tesla_device.measurement + if units == "F": + return TEMP_FAHRENHEIT + if units == "C": + return TEMP_CELSIUS + if units == "LENGTH_MILES": + return LENGTH_MILES + if units == "LENGTH_KILOMETERS": + return LENGTH_KILOMETERS + return units + + @property + def device_class(self) -> Optional[str]: + """Return the device_class of the device.""" + return ( + self.tesla_device.device_class + if self.tesla_device.device_class in DEVICE_CLASSES + else None + ) + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = self._attributes.copy() + if self.tesla_device.type == "charging rate sensor": + attr.update( + { + "time_left": self.tesla_device.time_left, + "added_range": self.tesla_device.added_range, + "charge_energy_added": self.tesla_device.charge_energy_added, + "charge_current_request": self.tesla_device.charge_current_request, + "charger_actual_current": self.tesla_device.charger_actual_current, + "charger_voltage": self.tesla_device.charger_voltage, + } + ) + return attr diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 5e9c2aa9031..cb57c1e3d5c 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.switch import SwitchEntity -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import STATE_ON from . import DOMAIN as TESLA_DOMAIN, TeslaDevice @@ -11,111 +11,95 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" - controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] + coordinator = hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"] entities = [] for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["switch"]: if device.type == "charger switch": - entities.append(ChargerSwitch(device, controller, config_entry)) - entities.append(UpdateSwitch(device, controller, config_entry)) + entities.append(ChargerSwitch(device, coordinator)) + entities.append(UpdateSwitch(device, coordinator)) elif device.type == "maxrange switch": - entities.append(RangeSwitch(device, controller, config_entry)) + entities.append(RangeSwitch(device, coordinator)) elif device.type == "sentry mode switch": - entities.append(SentryModeSwitch(device, controller, config_entry)) + entities.append(SentryModeSwitch(device, coordinator)) async_add_entities(entities, True) class ChargerSwitch(TeslaDevice, SwitchEntity): """Representation of a Tesla charger switch.""" - def __init__(self, tesla_device, controller, config_entry): - """Initialise of the switch.""" - self._state = None - super().__init__(tesla_device, controller, config_entry) - async def async_turn_on(self, **kwargs): """Send the on command.""" - _LOGGER.debug("Enable charging: %s", self._name) + _LOGGER.debug("Enable charging: %s", self.name) await self.tesla_device.start_charge() async def async_turn_off(self, **kwargs): """Send the off command.""" - _LOGGER.debug("Disable charging for: %s", self._name) + _LOGGER.debug("Disable charging for: %s", self.name) await self.tesla_device.stop_charge() @property def is_on(self): """Get whether the switch is in on state.""" - return self._state == STATE_ON - - async def async_update(self): - """Update the state of the switch.""" - _LOGGER.debug("Updating state for: %s", self._name) - await super().async_update() - self._state = STATE_ON if self.tesla_device.is_charging() else STATE_OFF + if self.tesla_device.is_charging() is None: + return None + return self.tesla_device.is_charging() == STATE_ON class RangeSwitch(TeslaDevice, SwitchEntity): """Representation of a Tesla max range charging switch.""" - def __init__(self, tesla_device, controller, config_entry): - """Initialise the switch.""" - self._state = None - super().__init__(tesla_device, controller, config_entry) - async def async_turn_on(self, **kwargs): """Send the on command.""" - _LOGGER.debug("Enable max range charging: %s", self._name) + _LOGGER.debug("Enable max range charging: %s", self.name) await self.tesla_device.set_max() async def async_turn_off(self, **kwargs): """Send the off command.""" - _LOGGER.debug("Disable max range charging: %s", self._name) + _LOGGER.debug("Disable max range charging: %s", self.name) await self.tesla_device.set_standard() @property def is_on(self): """Get whether the switch is in on state.""" - return self._state - - async def async_update(self): - """Update the state of the switch.""" - _LOGGER.debug("Updating state for: %s", self._name) - await super().async_update() - self._state = bool(self.tesla_device.is_maxrange()) + if self.tesla_device.is_maxrange() is None: + return None + return bool(self.tesla_device.is_maxrange()) class UpdateSwitch(TeslaDevice, SwitchEntity): """Representation of a Tesla update switch.""" - def __init__(self, tesla_device, controller, config_entry): + def __init__(self, tesla_device, coordinator): """Initialise the switch.""" - self._state = None - tesla_device.type = "update switch" - super().__init__(tesla_device, controller, config_entry) - self._name = self._name.replace("charger", "update") - self.tesla_id = self.tesla_id.replace("charger", "update") + super().__init__(tesla_device, coordinator) + self.controller = coordinator.controller + + @property + def name(self): + """Return the name of the device.""" + return super().name.replace("charger", "update") + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return super().unique_id.replace("charger", "update") async def async_turn_on(self, **kwargs): """Send the on command.""" - _LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id()) + _LOGGER.debug("Enable updates: %s %s", self.name, self.tesla_device.id()) self.controller.set_updates(self.tesla_device.id(), True) async def async_turn_off(self, **kwargs): """Send the off command.""" - _LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id()) + _LOGGER.debug("Disable updates: %s %s", self.name, self.tesla_device.id()) self.controller.set_updates(self.tesla_device.id(), False) @property def is_on(self): """Get whether the switch is in on state.""" - return self._state - - async def async_update(self): - """Update the state of the switch.""" - car_id = self.tesla_device.id() - _LOGGER.debug("Updating state for: %s %s", self._name, car_id) - await super().async_update() - self._state = bool(self.controller.get_updates(car_id)) + if self.controller.get_updates(self.tesla_device.id()) is None: + return None + return bool(self.controller.get_updates(self.tesla_device.id())) class SentryModeSwitch(TeslaDevice, SwitchEntity): @@ -123,25 +107,17 @@ class SentryModeSwitch(TeslaDevice, SwitchEntity): async def async_turn_on(self, **kwargs): """Send the on command.""" - _LOGGER.debug("Enable sentry mode: %s", self._name) + _LOGGER.debug("Enable sentry mode: %s", self.name) await self.tesla_device.enable_sentry_mode() async def async_turn_off(self, **kwargs): """Send the off command.""" - _LOGGER.debug("Disable sentry mode: %s", self._name) + _LOGGER.debug("Disable sentry mode: %s", self.name) await self.tesla_device.disable_sentry_mode() @property def is_on(self): """Get whether the switch is in on state.""" + if self.tesla_device.is_on() is None: + return None return self.tesla_device.is_on() - - @property - def available(self): - """Indicate if Home Assistant is able to read the state and control the underlying device.""" - return self.tesla_device.available() - - async def async_update(self): - """Update the state of the switch.""" - _LOGGER.debug("Updating state for: %s", self._name) - await super().async_update() diff --git a/requirements_all.txt b/requirements_all.txt index 074acb862bc..2b937acf508 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2107,7 +2107,7 @@ temperusb==1.5.3 tesla-powerwall==0.2.12 # homeassistant.components.tesla -teslajsonpy==0.10.1 +teslajsonpy==0.10.3 # homeassistant.components.tensorflow # tf-models-official==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c65f8b240d..6ef4f90aa80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -936,7 +936,7 @@ tellduslive==0.10.11 tesla-powerwall==0.2.12 # homeassistant.components.tesla -teslajsonpy==0.10.1 +teslajsonpy==0.10.3 # homeassistant.components.toon toonapi==0.2.0 From c3e77487da4f5dea3283495b3a33796148b1acc9 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 7 Aug 2020 20:58:31 -0700 Subject: [PATCH 038/862] Bump teslajsonpy to 0.10.4 (#38652) --- 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 11435ad2394..f1f4df6edd6 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.10.3"], + "requirements": ["teslajsonpy==0.10.4"], "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b937acf508..33e914cebca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2107,7 +2107,7 @@ temperusb==1.5.3 tesla-powerwall==0.2.12 # homeassistant.components.tesla -teslajsonpy==0.10.3 +teslajsonpy==0.10.4 # homeassistant.components.tensorflow # tf-models-official==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ef4f90aa80..70a24947fb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -936,7 +936,7 @@ tellduslive==0.10.11 tesla-powerwall==0.2.12 # homeassistant.components.tesla -teslajsonpy==0.10.3 +teslajsonpy==0.10.4 # homeassistant.components.toon toonapi==0.2.0 From f8570438e9e7d27f5e3dd4e732a34ac7b6876cf0 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 7 Aug 2020 23:50:08 -0500 Subject: [PATCH 039/862] Fix AccuWeather async timeout (#38654) --- homeassistant/components/accuweather/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 3dbb713ab2b..1e1a434a036 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -114,7 +114,7 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via library.""" try: - with timeout(10): + async with timeout(10): current = await self.accuweather.async_get_current_conditions() forecast = ( await self.accuweather.async_get_forecast(metric=self.is_metric) From 937b951727ebb2c193d4228efd66e0a1c75c07bf Mon Sep 17 00:00:00 2001 From: Alejandro Rivera Date: Sat, 8 Aug 2020 01:56:39 -0700 Subject: [PATCH 040/862] Fix rest_command UnboundLocalError in exception handling (#38656) ``` 2020-08-07 22:38:10 ERROR (MainThread) [homeassistant.components.websocket_api.http.connection.3903193064] local variable 'response' referenced before assignment Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/components/rest_command/__init__.py", line 115, in async_service_handler async with getattr(websession, method)( File "/usr/local/lib/python3.8/site-packages/aiohttp/client.py", line 1012, in __aenter__ self._resp = await self._coro File "/usr/local/lib/python3.8/site-packages/aiohttp/client.py", line 582, in _request break File "/usr/local/lib/python3.8/site-packages/aiohttp/helpers.py", line 586, in __exit__ raise asyncio.TimeoutError from None asyncio.exceptions.TimeoutError During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 125, in handle_call_service await hass.services.async_call( File "/usr/src/homeassistant/homeassistant/core.py", line 1281, in async_call task.result() File "/usr/src/homeassistant/homeassistant/core.py", line 1316, in _execute_service await handler.func(service_call) File "/usr/src/homeassistant/homeassistant/components/rest_command/__init__.py", line 137, in async_service_handler _LOGGER.warning("Timeout call %s", response.url, exc_info=1) UnboundLocalError: local variable 'response' referenced before assignment ``` --- homeassistant/components/rest_command/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index f8b99c48a44..1290912897d 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -134,7 +134,7 @@ async def async_setup(hass, config): ) except asyncio.TimeoutError: - _LOGGER.warning("Timeout call %s", response.url, exc_info=1) + _LOGGER.warning("Timeout call %s", request_url, exc_info=1) except aiohttp.ClientError: _LOGGER.error("Client error %s", request_url, exc_info=1) From d49c55a8daa63d8432806dd377d40945896a6595 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 8 Aug 2020 12:36:12 +0200 Subject: [PATCH 041/862] Bump pyupgrade to v2.7.2 (#38629) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a139e959a8..32b29ce54db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.3.0 + rev: v2.7.2 hooks: - id: pyupgrade args: [--py37-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 2574cb895e9..91cf10b4b2b 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -7,5 +7,5 @@ flake8-docstrings==1.5.0 flake8==3.8.3 isort==4.3.21 pydocstyle==5.0.2 -pyupgrade==2.3.0 +pyupgrade==2.7.2 yamllint==1.23.0 From e258ab7ff06b6b3f93087f4fb2b4205976c72be0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 8 Aug 2020 13:18:37 +0200 Subject: [PATCH 042/862] Bump yamllint to v1.24.2 (#38633) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32b29ce54db..498774e748d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -54,7 +54,7 @@ repos: - --branch=master - --branch=rc - repo: https://github.com/adrienverge/yamllint.git - rev: v1.23.0 + rev: v1.24.2 hooks: - id: yamllint - repo: https://github.com/prettier/prettier diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 91cf10b4b2b..55748aaca44 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -8,4 +8,4 @@ flake8==3.8.3 isort==4.3.21 pydocstyle==5.0.2 pyupgrade==2.7.2 -yamllint==1.23.0 +yamllint==1.24.2 From da89fa7884d3b3c705b01a1057496712873fd378 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Aug 2020 13:59:53 +0200 Subject: [PATCH 043/862] Fix xiaomi_aqara discovery (#38622) --- homeassistant/components/xiaomi_aqara/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index fb66be76635..c42598c2665 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -75,8 +75,9 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.interface = user_input[CONF_INTERFACE] # allow optional manual setting of host and mac - if self.host is None and self.sid is None: + if self.host is None: self.host = user_input.get(CONF_HOST) + if self.sid is None: mac_address = user_input.get(CONF_MAC) # format sid from mac_address @@ -173,7 +174,9 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): unique_id = mac_address await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured({CONF_HOST: self.host}) + self._abort_if_unique_id_configured( + {CONF_HOST: self.host, CONF_MAC: mac_address} + ) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": {"name": self.host}}) From 0427d87ba445bdd7646a4998ef6f3789aa1001e4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 8 Aug 2020 14:41:02 +0200 Subject: [PATCH 044/862] Bump codespell from v1.16.0 to v1.17.1 and fix new spelling errors (#38663) --- .pre-commit-config.yaml | 4 ++-- homeassistant/components/august/exceptions.py | 2 +- homeassistant/components/cloud/alexa_config.py | 2 +- homeassistant/components/hassio/discovery.py | 2 +- homeassistant/components/samsungtv/media_player.py | 2 +- homeassistant/components/velbus/config_flow.py | 2 +- requirements_test_pre_commit.txt | 2 +- tests/components/device_automation/test_init.py | 2 +- tests/components/heos/test_media_player.py | 4 ++-- tests/components/recorder/test_util.py | 2 +- tests/components/system_log/test_init.py | 4 ++-- tests/components/tplink/test_common.py | 2 +- tests/test_core.py | 4 ++-- tests/util/test_yaml.py | 2 +- 14 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 498774e748d..311c01ba1a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,11 +13,11 @@ repos: - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell - rev: v1.16.0 + rev: v1.17.1 hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing + - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/homeassistant/components/august/exceptions.py b/homeassistant/components/august/exceptions.py index 78c467ab3a1..edd418c9519 100644 --- a/homeassistant/components/august/exceptions.py +++ b/homeassistant/components/august/exceptions.py @@ -1,4 +1,4 @@ -"""Shared excecption for the august integration.""" +"""Shared exceptions for the august integration.""" from homeassistant import exceptions diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index a45469c8f97..3afb0ce2e86 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -259,7 +259,7 @@ class AlexaConfig(alexa_config.AbstractConfig): return True except asyncio.TimeoutError: - _LOGGER.warning("Timeout trying to sync entitites to Alexa") + _LOGGER.warning("Timeout trying to sync entities to Alexa") return False except aiohttp.ClientError as err: diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index fc6efbe0e58..f3337254f1a 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -66,7 +66,7 @@ class HassIODiscovery(HomeAssistantView): try: data = await self.hassio.get_discovery_message(uuid) except HassioAPIError as err: - _LOGGER.error("Can't read discovey data: %s", err) + _LOGGER.error("Can't read discovery data: %s", err) raise HTTPServiceUnavailable() from None await self.async_process_new(data) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 7eb0f50efc2..774027776c4 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -104,7 +104,7 @@ class SamsungTVDevice(MediaPlayerEntity): self._bridge.register_reauth_callback(self.access_denied) def access_denied(self): - """Access denied callbck.""" + """Access denied callback.""" LOGGER.debug("Access denied in getting remote object") self.hass.add_job( self.hass.config_entries.flow.async_init( diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index e85422d740a..361b5a01175 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -29,7 +29,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._errors = {} def _create_device(self, name: str, prt: str): - """Create an antry async.""" + """Create an entry async.""" return self.async_create_entry(title=name, data={CONF_PORT: prt}) def _test_connection(self, prt): diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 55748aaca44..62c8fa113de 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,7 +2,7 @@ bandit==1.6.2 black==19.10b0 -codespell==1.16.0 +codespell==1.17.1 flake8-docstrings==1.5.0 flake8==3.8.3 isort==4.3.21 diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index d6a9fda00bc..19786bb08e8 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -764,7 +764,7 @@ async def test_automation_with_bad_trigger(hass, caplog): async def test_websocket_device_not_found(hass, hass_ws_client): - """Test caling command with unknown device.""" + """Test calling command with unknown device.""" await async_setup_component(hass, "device_automation", {}) client = await hass_ws_client(hass) await client.send_json( diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 835f34e5efc..b2d96707d7e 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -694,7 +694,7 @@ async def test_play_media_playlist( await setup_platform(hass, config_entry, config) player = controller.players[1] playlist = playlists[0] - # Play without enqueing + # Play without enqueuing await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, @@ -708,7 +708,7 @@ async def test_play_media_playlist( player.add_to_queue.assert_called_once_with( playlist, const.ADD_QUEUE_REPLACE_AND_PLAY ) - # Play with enqueing + # Play with enqueuing player.add_to_queue.reset_mock() await hass.services.async_call( MEDIA_PLAYER_DOMAIN, diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 56f1e069a61..71bfc1e3bd4 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -50,7 +50,7 @@ def test_recorder_bad_execute(hass_recorder): hass_recorder() def to_native(validate_entity_id=True): - """Rasie exception.""" + """Raise exception.""" raise SQLAlchemyError() mck1 = MagicMock() diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index d19ca2261bb..d3f7447277c 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -180,8 +180,8 @@ def log_msg(nr=2): _LOGGER.error("error message %s", nr) -async def test_dedup_logs(hass, simple_queue, hass_client): - """Test that duplicate log entries are dedup.""" +async def test_dedupe_logs(hass, simple_queue, hass_client): + """Test that duplicate log entries are dedupe.""" await async_setup_component(hass, system_log.DOMAIN, {}) _LOGGER.error("error message 1") log_msg() diff --git a/tests/components/tplink/test_common.py b/tests/components/tplink/test_common.py index a2bd7ef87ff..9c219f1ba83 100644 --- a/tests/components/tplink/test_common.py +++ b/tests/components/tplink/test_common.py @@ -17,7 +17,7 @@ async def test_async_add_entities_retry(hass: HomeAssistantType): objects = ["Object 1", "Object 2", "Object 3", "Object 4"] # For each call to async_add_entities_callback, the following side effects - # will be triggered in order. This set of side effects accuratley simulates + # will be triggered in order. This set of side effects accurateley simulates # 3 attempts to add all entities while also handling several return types. # To help understand what's going on, a comment exists describing what the # object list looks like throughout the iterations. diff --git a/tests/test_core.py b/tests/test_core.py index 77baa502687..05a969d0a75 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -944,14 +944,14 @@ class TestConfig(unittest.TestCase): self.config.allowlist_external_dirs = {"/home", "/var"} - unvalid = [ + invalid = [ "/hass/config/secure", "/etc/passwd", "/root/secure_file", "/var/../etc/passwd", test_file, ] - for path in unvalid: + for path in invalid: assert not self.config.is_allowed_path(path) with pytest.raises(AssertionError): diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 4d6f4ce3ac9..96b6a86a27d 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -354,7 +354,7 @@ class TestSecrets(unittest.TestCase): assert expected == self._yaml["component"] def test_secrets_from_parent_folder(self): - """Test loading secrets from parent foler.""" + """Test loading secrets from parent folder.""" expected = {"api_password": "pwhttp"} self._yaml = load_yaml( os.path.join(self._sub_folder_path, "sub.yaml"), From 21e551b61dcbcd4e24ccb0fc336fc57e7cf69cee Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sat, 8 Aug 2020 15:01:57 +0200 Subject: [PATCH 045/862] Bump pre-commit-hooks from v2.40 to v3.2.0 (#38664) --- .pre-commit-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 311c01ba1a3..3a2b8c6da38 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -43,10 +43,9 @@ repos: hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v3.2.0 hooks: - id: check-executables-have-shebangs - stages: [manual] - id: check-json - id: no-commit-to-branch args: From 9169d73c7166a5b2e741126bb6797bb14cb68dac Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sat, 8 Aug 2020 10:34:24 -0400 Subject: [PATCH 046/862] Bump env_canada to 0.2.0 (#37467) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 3ef504f6e22..f89a7eb3e55 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,6 +2,6 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.1.0"], + "requirements": ["env_canada==0.2.0"], "codeowners": ["@michaeldavie"] } diff --git a/requirements_all.txt b/requirements_all.txt index 33e914cebca..aef353640b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -539,7 +539,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -env_canada==0.1.0 +env_canada==0.2.0 # homeassistant.components.envirophat # envirophat==0.0.6 From 1cb161017e26c9b59df58c1126490dd18d201467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 8 Aug 2020 20:00:56 +0200 Subject: [PATCH 047/862] Update frontend to 20200807.1 (#38626) --- 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 c4d11297e9b..aaab3ac570c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200805.0"], + "requirements": ["home-assistant-frontend==20200807.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0339c779291..6b4deee3a3e 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.35.0 -home-assistant-frontend==20200805.0 +home-assistant-frontend==20200807.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 netdisco==2.8.1 diff --git a/requirements_all.txt b/requirements_all.txt index aef353640b1..e3576d5a46e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -733,7 +733,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200805.0 +home-assistant-frontend==20200807.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70a24947fb2..71b20920545 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -362,7 +362,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200805.0 +home-assistant-frontend==20200807.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 74c23c3e962bf12021f2ce465ef8ebab6c83ed89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Aug 2020 13:23:56 -0500 Subject: [PATCH 048/862] Add support for reload_on_update to _abort_if_unique_id_configured (#38638) * Add support for reload_on_update to _abort_if_unique_id_configured async_update_entry now avoids firing update listeners and writing the storage if there are no actual changes. * Actually add the tests * collapse branch * Update homeassistant/config_entries.py Co-authored-by: Franck Nijhof * handle entries that lack the ability to reload * reduce * adjust konnected tests * update axis tests * fix blocking * more mocking * config flow tests outside of test_config_flow * reduce * volumio * Update homeassistant/config_entries.py Co-authored-by: Paulus Schoutsen * set reload_on_update=False for integrations that implement self._abort_if_unique_id_configured(updates= and a reload listen * get rid of copy * revert test change Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen --- homeassistant/components/hue/config_flow.py | 4 +- .../components/konnected/config_flow.py | 4 +- homeassistant/config_entries.py | 46 ++++-- tests/components/axis/test_config_flow.py | 35 +++-- tests/components/axis/test_device.py | 26 ++-- tests/components/deconz/test_config_flow.py | 63 +++++--- tests/components/deconz/test_gateway.py | 26 ++-- tests/components/volumio/test_config_flow.py | 15 +- tests/test_config_entries.py | 135 ++++++++++++++++-- 9 files changed, 272 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 66c93ead846..f16d1f74cf6 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -198,7 +198,9 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): bridge = self._async_get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) await self.async_set_unique_id(bridge.id) - self._abort_if_unique_id_configured(updates={CONF_HOST: bridge.host}) + self._abort_if_unique_id_configured( + updates={CONF_HOST: bridge.host}, reload_on_update=False + ) self.bridge = bridge return await self.async_step_link() diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index f545c5f2f2a..c9cbfe03ae7 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -329,7 +329,9 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: # abort and update an existing config entry if host info changes await self.async_set_unique_id(self.data[CONF_ID]) - self._abort_if_unique_id_configured(updates=self.data) + self._abort_if_unique_id_configured( + updates=self.data, reload_on_update=False + ) return self.async_show_form( step_id="confirm", description_placeholders={ diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f36e2c6accb..90d9c623fac 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -750,23 +750,43 @@ class ConfigEntries: data: dict = _UNDEF, options: dict = _UNDEF, system_options: dict = _UNDEF, - ) -> None: - """Update a config entry.""" - if unique_id is not _UNDEF: + ) -> bool: + """Update a config entry. + + If the entry was changed, the update_listeners are + fired and this function returns True + + If the entry was not changed, the update_listeners are + not fired and this function returns False + """ + changed = False + + if unique_id is not _UNDEF and entry.unique_id != unique_id: + changed = True entry.unique_id = cast(Optional[str], unique_id) - if title is not _UNDEF: + if title is not _UNDEF and entry.title != title: + changed = True entry.title = cast(str, title) - if data is not _UNDEF: + if data is not _UNDEF and entry.data != data: # type: ignore + changed = True entry.data = MappingProxyType(data) - if options is not _UNDEF: + if options is not _UNDEF and entry.options != options: # type: ignore + changed = True entry.options = MappingProxyType(options) - if system_options is not _UNDEF: + if ( + system_options is not _UNDEF + and entry.system_options.as_dict() != system_options + ): + changed = True entry.system_options.update(**system_options) + if not changed: + return False + for listener_ref in entry.update_listeners: listener = listener_ref() if listener is not None: @@ -774,6 +794,8 @@ class ConfigEntries: self._async_schedule_save() + return True + async def async_forward_entry_setup(self, entry: ConfigEntry, domain: str) -> bool: """Forward the setup of an entry to a different component. @@ -850,7 +872,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def _abort_if_unique_id_configured( - self, updates: Optional[Dict[Any, Any]] = None + self, updates: Optional[Dict[Any, Any]] = None, reload_on_update: bool = True, ) -> None: """Abort if the unique ID is already configured.""" assert self.hass @@ -859,10 +881,14 @@ class ConfigFlow(data_entry_flow.FlowHandler): for entry in self._async_current_entries(): if entry.unique_id == self.unique_id: - if updates is not None and not updates.items() <= entry.data.items(): - self.hass.config_entries.async_update_entry( + if updates is not None: + changed = self.hass.config_entries.async_update_entry( entry, data={**entry.data, **updates} ) + if changed and reload_on_update: + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) raise data_entry_flow.AbortFlow("already_configured") async def async_set_unique_id( diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index aa7d9db9027..df0ee32389a 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -67,7 +67,11 @@ async def test_manual_configuration_update_configuration(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch("axis.vapix.session_request", new=vapix_session_request): + with patch( + "homeassistant.components.axis.async_setup_entry", return_value=True, + ) as mock_setup_entry, patch( + "axis.vapix.session_request", new=vapix_session_request + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -77,10 +81,12 @@ async def test_manual_configuration_update_configuration(hass): CONF_PORT: 80, }, ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" assert device.host == "2.3.4.5" + assert len(mock_setup_entry.mock_calls) == 1 async def test_flow_fails_already_configured(hass): @@ -282,16 +288,22 @@ async def test_zeroconf_flow_updated_configuration(hass): CONF_NAME: NAME, } - result = await hass.config_entries.flow.async_init( - AXIS_DOMAIN, - data={ - CONF_HOST: "2.3.4.5", - CONF_PORT: 8080, - "hostname": "name", - "properties": {"macaddress": MAC}, - }, - context={"source": "zeroconf"}, - ) + with patch( + "homeassistant.components.axis.async_setup_entry", return_value=True, + ) as mock_setup_entry, patch( + "axis.vapix.session_request", new=vapix_session_request + ): + result = await hass.config_entries.flow.async_init( + AXIS_DOMAIN, + data={ + CONF_HOST: "2.3.4.5", + CONF_PORT: 8080, + "hostname": "name", + "properties": {"macaddress": MAC}, + }, + context={"source": "zeroconf"}, + ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -304,6 +316,7 @@ async def test_zeroconf_flow_updated_configuration(hass): CONF_MODEL: MODEL, CONF_NAME: NAME, } + assert len(mock_setup_entry.mock_calls) == 1 async def test_zeroconf_flow_ignore_non_axis_device(hass): diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 4350764c486..c8583a8ce03 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -290,19 +290,23 @@ async def test_update_address(hass): device = await setup_axis_integration(hass) assert device.api.config.host == "1.2.3.4" - await hass.config_entries.flow.async_init( - AXIS_DOMAIN, - data={ - "host": "2.3.4.5", - "port": 80, - "hostname": "name", - "properties": {"macaddress": MAC}, - }, - context={"source": "zeroconf"}, - ) - await hass.async_block_till_done() + with patch("axis.vapix.session_request", new=vapix_session_request), patch( + "homeassistant.components.axis.async_setup_entry", return_value=True, + ) as mock_setup_entry: + await hass.config_entries.flow.async_init( + AXIS_DOMAIN, + data={ + "host": "2.3.4.5", + "port": 80, + "hostname": "name", + "properties": {"macaddress": MAC}, + }, + context={"source": "zeroconf"}, + ) + await hass.async_block_till_done() assert device.api.config.host == "2.3.4.5" + assert len(mock_setup_entry.mock_calls) == 1 async def test_device_unavailable(hass): diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index b29e4303f7b..0a536bcda5b 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for deCONZ config flow.""" import asyncio +from asynctest.mock import patch import pydeconz from homeassistant import data_entry_flow @@ -399,19 +400,24 @@ async def test_ssdp_discovery_update_configuration(hass): """Test if a discovered bridge is configured but updates with new attributes.""" gateway = await setup_deconz_integration(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", - ssdp.ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, - ssdp.ATTR_UPNP_SERIAL: BRIDGEID, - }, - context={"source": "ssdp"}, - ) + with patch( + "homeassistant.components.deconz.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: BRIDGEID, + }, + context={"source": "ssdp"}, + ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert gateway.config_entry.data[CONF_HOST] == "2.3.4.5" + assert len(mock_setup_entry.mock_calls) == 1 async def test_ssdp_discovery_dont_update_configuration(hass): @@ -469,9 +475,15 @@ async def test_flow_hassio_discovery(hass): assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) + with patch( + "homeassistant.components.deconz.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.deconz.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["result"].data == { @@ -479,28 +491,35 @@ async def test_flow_hassio_discovery(hass): CONF_PORT: 80, CONF_API_KEY: API_KEY, } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 async def test_hassio_discovery_update_configuration(hass): """Test we can update an existing config entry.""" gateway = await setup_deconz_integration(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={ - CONF_HOST: "2.3.4.5", - CONF_PORT: 8080, - CONF_API_KEY: "updated", - CONF_SERIAL: BRIDGEID, - }, - context={"source": "hassio"}, - ) + with patch( + "homeassistant.components.deconz.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={ + CONF_HOST: "2.3.4.5", + CONF_PORT: 8080, + CONF_API_KEY: "updated", + CONF_SERIAL: BRIDGEID, + }, + context={"source": "hassio"}, + ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert gateway.config_entry.data[CONF_HOST] == "2.3.4.5" assert gateway.config_entry.data[CONF_PORT] == 8080 assert gateway.config_entry.data[CONF_API_KEY] == "updated" + assert len(mock_setup_entry.mock_calls) == 1 async def test_hassio_discovery_dont_update_configuration(hass): diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 9af1a3151e8..4236888a5a6 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -135,19 +135,23 @@ async def test_update_address(hass): gateway = await setup_deconz_integration(hass) assert gateway.api.host == "1.2.3.4" - await hass.config_entries.flow.async_init( - deconz.config_flow.DOMAIN, - data={ - ssdp.ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", - ssdp.ATTR_UPNP_MANUFACTURER_URL: deconz.config_flow.DECONZ_MANUFACTURERURL, - ssdp.ATTR_UPNP_SERIAL: BRIDGEID, - ssdp.ATTR_UPNP_UDN: "uuid:456DEF", - }, - context={"source": "ssdp"}, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.deconz.async_setup_entry", return_value=True, + ) as mock_setup_entry: + await hass.config_entries.flow.async_init( + deconz.config_flow.DOMAIN, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: deconz.config_flow.DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: BRIDGEID, + ssdp.ATTR_UPNP_UDN: "uuid:456DEF", + }, + context={"source": "ssdp"}, + ) + await hass.async_block_till_done() assert gateway.api.host == "2.3.4.5" + assert len(mock_setup_entry.mock_calls) == 1 async def test_reset_after_successful_setup(hass): diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index a7ed4773142..6fc7390da79 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -85,6 +85,7 @@ async def test_form_updates_unique_id(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CONNECTION, ) + await hass.async_block_till_done() assert result2["type"] == "abort" assert result2["reason"] == "already_configured" @@ -242,11 +243,19 @@ async def test_discovery_updates_unique_id(hass): entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY - ) + with patch( + "homeassistant.components.volumio.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.volumio.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data == TEST_DISCOVERY_RESULT + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 6d513697daf..ab28ecc7af3 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -624,10 +624,10 @@ async def test_updating_entry_data(manager): ) entry.add_to_manager(manager) - manager.async_update_entry(entry) + assert manager.async_update_entry(entry) is False assert entry.data == {"first": True} - manager.async_update_entry(entry, data={"second": True}) + assert manager.async_update_entry(entry, data={"second": True}) is True assert entry.data == {"second": True} @@ -658,7 +658,7 @@ async def test_update_entry_options_and_trigger_listener(hass, manager): entry.add_update_listener(update_listener) - manager.async_update_entry(entry, options={"second": True}) + assert manager.async_update_entry(entry, options={"second": True}) is True assert entry.options == {"second": True} @@ -1120,7 +1120,7 @@ async def test_unique_id_existing_entry(hass, manager): assert len(async_remove_entry.mock_calls) == 1 -async def test_unique_id_update_existing_entry(hass, manager): +async def test_unique_id_update_existing_entry_without_reload(hass, manager): """Test that we update an entry if there already is an entry with unique ID.""" hass.config.components.add("comp") entry = MockConfigEntry( @@ -1143,17 +1143,65 @@ async def test_unique_id_update_existing_entry(hass, manager): async def async_step_user(self, user_input=None): """Test user step.""" await self.async_set_unique_id("mock-unique-id") - await self._abort_if_unique_id_configured(updates={"host": "1.1.1.1"}) + await self._abort_if_unique_id_configured( + updates={"host": "1.1.1.1"}, reload_on_update=False + ) - with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload: result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" + assert len(async_reload.mock_calls) == 0 + + +async def test_unique_id_update_existing_entry_with_reload(hass, manager): + """Test that we update an entry if there already is an entry with unique ID and we reload on changes.""" + hass.config.components.add("comp") + entry = MockConfigEntry( + domain="comp", + data={"additional": "data", "host": "0.0.0.0"}, + unique_id="mock-unique-id", + ) + entry.add_to_hass(hass) + + mock_integration( + hass, MockModule("comp"), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + await self.async_set_unique_id("mock-unique-id") + await self._abort_if_unique_id_configured( + updates={"host": "1.1.1.1"}, reload_on_update=True + ) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload: + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data["host"] == "1.1.1.1" + assert entry.data["additional"] == "data" + assert len(async_reload.mock_calls) == 1 async def test_unique_id_not_update_existing_entry(hass, manager): @@ -1179,20 +1227,23 @@ async def test_unique_id_not_update_existing_entry(hass, manager): async def async_step_user(self, user_input=None): """Test user step.""" await self.async_set_unique_id("mock-unique-id") - await self._abort_if_unique_id_configured(updates={"host": "0.0.0.0"}) + await self._abort_if_unique_id_configured( + updates={"host": "0.0.0.0"}, reload_on_update=True + ) with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( - "homeassistant.config_entries.ConfigEntries.async_update_entry" - ) as async_update_entry: + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload: result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) + await hass.async_block_till_done() assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data["host"] == "0.0.0.0" assert entry.data["additional"] == "data" - assert len(async_update_entry.mock_calls) == 0 + assert len(async_reload.mock_calls) == 0 async def test_unique_id_in_progress(hass, manager): @@ -1567,8 +1618,11 @@ async def test_async_setup_update_entry(hass): async def async_step_import(self, user_input): """Test import step updating existing entry.""" - self.hass.config_entries.async_update_entry( - entry, data={"value": "updated"} + assert ( + self.hass.config_entries.async_update_entry( + entry, data={"value": "updated"} + ) + is True ) return self.async_abort(reason="yo") @@ -1758,3 +1812,60 @@ async def test_default_discovery_abort_on_new_unique_flow(hass, manager): flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["context"]["unique_id"] == "mock-unique-id" + + +async def test_updating_entry_with_and_without_changes(manager): + """Test that we can update an entry data.""" + entry = MockConfigEntry( + domain="test", + data={"first": True}, + title="thetitle", + options={"option": True}, + unique_id="abc123", + state=config_entries.ENTRY_STATE_SETUP_ERROR, + ) + entry.add_to_manager(manager) + + assert manager.async_update_entry(entry) is False + assert manager.async_update_entry(entry, data={"second": True}) is True + assert manager.async_update_entry(entry, data={"second": True}) is False + assert ( + manager.async_update_entry(entry, data={"second": True, "third": 456}) is True + ) + assert ( + manager.async_update_entry(entry, data={"second": True, "third": 456}) is False + ) + assert manager.async_update_entry(entry, options={"second": True}) is True + assert manager.async_update_entry(entry, options={"second": True}) is False + assert ( + manager.async_update_entry(entry, options={"second": True, "third": "123"}) + is True + ) + assert ( + manager.async_update_entry(entry, options={"second": True, "third": "123"}) + is False + ) + assert ( + manager.async_update_entry(entry, system_options={"disable_new_entities": True}) + is True + ) + assert ( + manager.async_update_entry(entry, system_options={"disable_new_entities": True}) + is False + ) + assert ( + manager.async_update_entry( + entry, system_options={"disable_new_entities": False} + ) + is True + ) + assert ( + manager.async_update_entry( + entry, system_options={"disable_new_entities": False} + ) + is False + ) + assert manager.async_update_entry(entry, title="thetitle") is False + assert manager.async_update_entry(entry, title="newtitle") is True + assert manager.async_update_entry(entry, unique_id="abc123") is False + assert manager.async_update_entry(entry, unique_id="abc1234") is True From 02d572aae5ee05002776e9722a40f3fdfbeaa7bf Mon Sep 17 00:00:00 2001 From: fabiocastagnino Date: Sat, 8 Aug 2020 21:04:18 +0200 Subject: [PATCH 049/862] Add device classes for electrical measurement (#36800) * added device classes for electrical measurement (cherry picked from commit 2409fe19ed43bef568a0cca826652867d3a2d71a) * upadte power factor unit (%) * update power factor unit (%) --- homeassistant/components/sensor/__init__.py | 8 ++++++++ .../components/sensor/device_condition.py | 16 ++++++++++++++++ .../components/sensor/device_trigger.py | 16 ++++++++++++++++ homeassistant/components/sensor/strings.json | 8 ++++++++ homeassistant/const.py | 4 ++++ tests/components/sensor/test_device_trigger.py | 2 +- .../custom_components/test/sensor.py | 4 ++++ 7 files changed, 57 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 83711a07759..46462019749 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -7,13 +7,17 @@ import voluptuous as vol from homeassistant.const import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, ) from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -32,6 +36,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=30) DEVICE_CLASSES = [ DEVICE_CLASS_BATTERY, # % of battery that is left + DEVICE_CLASS_CURRENT, # current (A) + DEVICE_CLASS_ENERGY, # energy (kWh, Wh) DEVICE_CLASS_HUMIDITY, # % of humidity in the air DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) DEVICE_CLASS_SIGNAL_STRENGTH, # signal strength (dB/dBm) @@ -39,6 +45,8 @@ DEVICE_CLASSES = [ DEVICE_CLASS_TIMESTAMP, # timestamp (ISO8601) DEVICE_CLASS_PRESSURE, # pressure (hPa/mbar) DEVICE_CLASS_POWER, # power (W/kW) + DEVICE_CLASS_POWER_FACTOR, # power factor (%) + DEVICE_CLASS_VOLTAGE, # voltage (V) ] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 3c4337c1dba..d58381a2f40 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -14,13 +14,17 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_TYPE, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv @@ -37,24 +41,32 @@ from . import DOMAIN DEVICE_CLASS_NONE = "none" CONF_IS_BATTERY_LEVEL = "is_battery_level" +CONF_IS_CURRENT = "is_current" +CONF_IS_ENERGY = "is_energy" CONF_IS_HUMIDITY = "is_humidity" CONF_IS_ILLUMINANCE = "is_illuminance" CONF_IS_POWER = "is_power" +CONF_IS_POWER_FACTOR = "is_power_factor" CONF_IS_PRESSURE = "is_pressure" CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" CONF_IS_TEMPERATURE = "is_temperature" CONF_IS_TIMESTAMP = "is_timestamp" +CONF_IS_VOLTAGE = "is_voltage" CONF_IS_VALUE = "is_value" ENTITY_CONDITIONS = { DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], + DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_IS_CURRENT}], + DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}], + DEVICE_CLASS_POWER_FACTOR: [{CONF_TYPE: CONF_IS_POWER_FACTOR}], DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_IS_TIMESTAMP}], + DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}], } @@ -65,13 +77,17 @@ CONDITION_SCHEMA = vol.All( vol.Required(CONF_TYPE): vol.In( [ CONF_IS_BATTERY_LEVEL, + CONF_IS_CURRENT, + CONF_IS_ENERGY, CONF_IS_HUMIDITY, CONF_IS_ILLUMINANCE, CONF_IS_POWER, + CONF_IS_POWER_FACTOR, CONF_IS_PRESSURE, CONF_IS_SIGNAL_STRENGTH, CONF_IS_TEMPERATURE, CONF_IS_TIMESTAMP, + CONF_IS_VOLTAGE, CONF_IS_VALUE, ] ), diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 57c32da97e9..48c0ec493c9 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -15,13 +15,17 @@ from homeassistant.const import ( CONF_FOR, CONF_TYPE, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLTAGE, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device @@ -33,24 +37,32 @@ from . import DOMAIN DEVICE_CLASS_NONE = "none" CONF_BATTERY_LEVEL = "battery_level" +CONF_CURRENT = "current" +CONF_ENERGY = "energy" CONF_HUMIDITY = "humidity" CONF_ILLUMINANCE = "illuminance" CONF_POWER = "power" +CONF_POWER_FACTOR = "power_factor" CONF_PRESSURE = "pressure" CONF_SIGNAL_STRENGTH = "signal_strength" CONF_TEMPERATURE = "temperature" CONF_TIMESTAMP = "timestamp" +CONF_VOLTAGE = "voltage" CONF_VALUE = "value" ENTITY_TRIGGERS = { DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], + DEVICE_CLASS_CURRENT: [{CONF_TYPE: CONF_CURRENT}], + DEVICE_CLASS_ENERGY: [{CONF_TYPE: CONF_ENERGY}], DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}], + DEVICE_CLASS_POWER_FACTOR: [{CONF_TYPE: CONF_POWER_FACTOR}], DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_TIMESTAMP}], + DEVICE_CLASS_VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], } @@ -62,13 +74,17 @@ TRIGGER_SCHEMA = vol.All( vol.Required(CONF_TYPE): vol.In( [ CONF_BATTERY_LEVEL, + CONF_CURRENT, + CONF_ENERGY, CONF_HUMIDITY, CONF_ILLUMINANCE, CONF_POWER, + CONF_POWER_FACTOR, CONF_PRESSURE, CONF_SIGNAL_STRENGTH, CONF_TEMPERATURE, CONF_TIMESTAMP, + CONF_VOLTAGE, CONF_VALUE, ] ), diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 024f942280c..76ea9efabc3 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -10,6 +10,10 @@ "is_signal_strength": "Current {entity_name} signal strength", "is_temperature": "Current {entity_name} temperature", "is_timestamp": "Current {entity_name} timestamp", + "is_current": "Current {entity_name} current", + "is_energy": "Current {entity_name} energy", + "is_power_factor": "Current {entity_name} power factor", + "is_voltage": "Current {entity_name} voltage", "is_value": "Current {entity_name} value" }, "trigger_type": { @@ -21,6 +25,10 @@ "signal_strength": "{entity_name} signal strength changes", "temperature": "{entity_name} temperature changes", "timestamp": "{entity_name} timestamp changes", + "current": "{entity_name} current changes", + "energy": "{entity_name} energy changes", + "power_factor": "{entity_name} power factor changes", + "voltage": "{entity_name} voltage changes", "value": "{entity_name} value changes" } }, diff --git a/homeassistant/const.py b/homeassistant/const.py index 8b075d37950..7ef23fa6903 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -218,6 +218,10 @@ DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_TIMESTAMP = "timestamp" DEVICE_CLASS_PRESSURE = "pressure" DEVICE_CLASS_POWER = "power" +DEVICE_CLASS_CURRENT = "current" +DEVICE_CLASS_ENERGY = "energy" +DEVICE_CLASS_POWER_FACTOR = "power_factor" +DEVICE_CLASS_VOLTAGE = "voltage" # #### STATES #### STATE_ON = "on" diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 3f44e9e5e32..9bdaf5b1fe0 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -76,7 +76,7 @@ async def test_get_triggers(hass, device_reg, entity_reg): if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert len(triggers) == 8 + assert len(triggers) == 12 assert triggers == expected_triggers diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 38bf9653938..b81cc34c34b 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -20,6 +20,10 @@ UNITS_OF_MEASUREMENT = { sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601) sensor.DEVICE_CLASS_PRESSURE: "hPa", # pressure (hPa/mbar) sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) + sensor.DEVICE_CLASS_CURRENT: "A", # current (A) + sensor.DEVICE_CLASS_ENERGY: "kWh", # energy (Wh/kWh) + sensor.DEVICE_CLASS_POWER_FACTOR: "%", # power factor (no unit, min: -1.0, max: 1.0) + sensor.DEVICE_CLASS_VOLTAGE: "V", # voltage (V) } ENTITIES = {} From e304792f3a7f459ab38000918d8bf7c56df565d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Aug 2020 15:37:05 -0500 Subject: [PATCH 050/862] Ensure shared zeroconf is passed to homekit controller devices (#38678) --- homeassistant/components/homekit_controller/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 47f3cf20571..4a8730b2e9e 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -11,6 +11,7 @@ from aiohomekit.model.characteristics import ( ) from aiohomekit.model.services import Service, ServicesTypes +from homeassistant.components import zeroconf from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import Entity @@ -212,7 +213,8 @@ async def async_setup(hass, config): map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) await map_storage.async_initialize() - hass.data[CONTROLLER] = aiohomekit.Controller() + zeroconf_instance = await zeroconf.async_get_instance(hass) + hass.data[CONTROLLER] = aiohomekit.Controller(zeroconf_instance=zeroconf_instance) hass.data[KNOWN_DEVICES] = {} return True From 34e2a1825bf117e236d5ae479486a0c2afe324fd Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 8 Aug 2020 14:28:04 -0700 Subject: [PATCH 051/862] Add support for exposing light effects via Google Assistant (#38575) * Don't set SUPPORT_EFFECT on DemoLight if there are no effects This requires an update to the group test - previously the other lights instantiated by the DemoLight component had nothing in ATTR_EFFECT_LIST, but still had SUPPORT_EFFECT set. This appears to have resulted in the light group test code setting an effect on the group and expecting it to apply to all lights, but given that two of the bulbs didn't actually support any effects (due to the empty ATTR_EFFECT_LIST) this seems like a broken assumption and updating the test to verify only the bulb that supports effects has had one applied seems reasonable. * Add support for exposing light effects via Google Assistant The LightEffects trait only supports a fixed (and small) list of lighting effects, but we can expose them via the Modes trait - this requires saying "Set (foo) effect to (bar)" which is a little clumsy, but at least makes it possible. --- homeassistant/components/demo/light.py | 11 +- .../components/google_assistant/trait.py | 21 ++++ tests/components/google_assistant/__init__.py | 1 + .../google_assistant/test_smart_home.py | 119 +++++++++++++++++- .../components/google_assistant/test_trait.py | 67 ++++++++++ tests/components/group/test_light.py | 2 - 6 files changed, 208 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 11b6a4812e8..640502584f8 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -24,11 +24,7 @@ LIGHT_EFFECT_LIST = ["rainbow", "none"] LIGHT_TEMPS = [240, 380] SUPPORT_DEMO = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_COLOR - | SUPPORT_WHITE_VALUE + SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_WHITE_VALUE ) @@ -81,10 +77,13 @@ class DemoLight(LightEntity): self._ct = ct or random.choice(LIGHT_TEMPS) self._brightness = brightness self._white = white + self._features = SUPPORT_DEMO self._effect_list = effect_list self._effect = effect self._available = True self._color_mode = "ct" if ct is not None and hs_color is None else "hs" + if self._effect_list is not None: + self._features |= SUPPORT_EFFECT @property def device_info(self): @@ -161,7 +160,7 @@ class DemoLight(LightEntity): @property def supported_features(self) -> int: """Flag supported features.""" - return SUPPORT_DEMO + return self._features async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 6ff19aedeb4..b347294f275 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1280,6 +1280,9 @@ class ModesTrait(_Trait): if domain == humidifier.DOMAIN and features & humidifier.SUPPORT_MODES: return True + if domain == light.DOMAIN and features & light.SUPPORT_EFFECT: + return True + if domain != media_player.DOMAIN: return False @@ -1317,6 +1320,7 @@ class ModesTrait(_Trait): (media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"), (input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"), (humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"), + (light.DOMAIN, light.ATTR_EFFECT_LIST, "effect"), ): if self.state.domain != domain: continue @@ -1347,6 +1351,9 @@ class ModesTrait(_Trait): elif self.state.domain == humidifier.DOMAIN: if humidifier.ATTR_MODE in attrs: mode_settings["mode"] = attrs.get(humidifier.ATTR_MODE) + elif self.state.domain == light.DOMAIN: + if light.ATTR_EFFECT in attrs: + mode_settings["effect"] = attrs.get(light.ATTR_EFFECT) if mode_settings: response["on"] = self.state.state not in (STATE_OFF, STATE_UNKNOWN) @@ -1386,6 +1393,20 @@ class ModesTrait(_Trait): ) return + if self.state.domain == light.DOMAIN: + requested_effect = settings["effect"] + await self.hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: self.state.entity_id, + light.ATTR_EFFECT: requested_effect, + }, + blocking=True, + context=data.context, + ) + return + if self.state.domain != media_player.DOMAIN: _LOGGER.info( "Received an Options command for unrecognised domain %s", diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index f665fa53ed2..f4e26a77f48 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -128,6 +128,7 @@ DEMO_DEVICES = [ "action.devices.traits.OnOff", "action.devices.traits.Brightness", "action.devices.traits.ColorSetting", + "action.devices.traits.Modes", ], "type": "action.devices.types.LIGHT", "willReportState": False, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6cd99d1fdd1..db97a42dff4 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -9,7 +9,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.cover import DemoCover -from homeassistant.components.demo.light import DemoLight +from homeassistant.components.demo.light import LIGHT_EFFECT_LIST, DemoLight from homeassistant.components.demo.media_player import AbstractDemoPlayer from homeassistant.components.demo.switch import DemoSwitch from homeassistant.components.google_assistant import ( @@ -48,7 +48,14 @@ def registries(hass): async def test_sync_message(hass): """Test a sync message.""" - light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) + light = DemoLight( + None, + "Demo Light", + state=False, + hs_color=(180, 75), + effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0], + ) light.hass = hass light.entity_id = "light.demo_light" await light.async_update_ha_state() @@ -95,10 +102,37 @@ async def test_sync_message(hass): trait.TRAIT_BRIGHTNESS, trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, + trait.TRAIT_MODES, ], "type": const.TYPE_LIGHT, "willReportState": False, "attributes": { + "availableModes": [ + { + "name": "effect", + "name_values": [ + {"lang": "en", "name_synonym": ["effect"]} + ], + "ordered": False, + "settings": [ + { + "setting_name": "rainbow", + "setting_values": [ + { + "lang": "en", + "setting_synonym": ["rainbow"], + } + ], + }, + { + "setting_name": "none", + "setting_values": [ + {"lang": "en", "setting_synonym": ["none"]} + ], + }, + ], + } + ], "colorModel": "hsv", "colorTemperatureRange": { "temperatureMinK": 2000, @@ -132,7 +166,14 @@ async def test_sync_in_area(hass, registries): "light", "test", "1235", suggested_object_id="demo_light", device_id=device.id ) - light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) + light = DemoLight( + None, + "Demo Light", + state=False, + hs_color=(180, 75), + effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0], + ) light.hass = hass light.entity_id = entity.entity_id await light.async_update_ha_state() @@ -162,10 +203,37 @@ async def test_sync_in_area(hass, registries): trait.TRAIT_BRIGHTNESS, trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, + trait.TRAIT_MODES, ], "type": const.TYPE_LIGHT, "willReportState": False, "attributes": { + "availableModes": [ + { + "name": "effect", + "name_values": [ + {"lang": "en", "name_synonym": ["effect"]} + ], + "ordered": False, + "settings": [ + { + "setting_name": "rainbow", + "setting_values": [ + { + "lang": "en", + "setting_synonym": ["rainbow"], + } + ], + }, + { + "setting_name": "none", + "setting_values": [ + {"lang": "en", "setting_synonym": ["none"]} + ], + }, + ], + } + ], "colorModel": "hsv", "colorTemperatureRange": { "temperatureMinK": 2000, @@ -186,7 +254,14 @@ async def test_sync_in_area(hass, registries): async def test_query_message(hass): """Test a sync message.""" - light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) + light = DemoLight( + None, + "Demo Light", + state=False, + hs_color=(180, 75), + effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0], + ) light.hass = hass light.entity_id = "light.demo_light" await light.async_update_ha_state() @@ -555,7 +630,14 @@ async def test_serialize_input_boolean(hass): async def test_unavailable_state_does_sync(hass): """Test that an unavailable entity does sync over.""" - light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) + light = DemoLight( + None, + "Demo Light", + state=False, + hs_color=(180, 75), + effect_list=LIGHT_EFFECT_LIST, + effect=LIGHT_EFFECT_LIST[0], + ) light.hass = hass light.entity_id = "light.demo_light" light._available = False # pylint: disable=protected-access @@ -584,10 +666,37 @@ async def test_unavailable_state_does_sync(hass): trait.TRAIT_BRIGHTNESS, trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, + trait.TRAIT_MODES, ], "type": const.TYPE_LIGHT, "willReportState": False, "attributes": { + "availableModes": [ + { + "name": "effect", + "name_values": [ + {"lang": "en", "name_synonym": ["effect"]} + ], + "ordered": False, + "settings": [ + { + "setting_name": "rainbow", + "setting_values": [ + { + "lang": "en", + "setting_synonym": ["rainbow"], + } + ], + }, + { + "setting_name": "none", + "setting_values": [ + {"lang": "en", "setting_synonym": ["none"]} + ], + }, + ], + } + ], "colorModel": "hsv", "colorTemperatureRange": { "temperatureMinK": 2000, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 54a8acf1a65..9d821b357ca 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -515,6 +515,73 @@ async def test_color_light_temperature_light_bad_temp(hass): assert trt.query_attributes() == {} +async def test_light_modes(hass): + """Test Light Mode trait.""" + assert helpers.get_google_type(light.DOMAIN, None) is not None + assert trait.ModesTrait.supported(light.DOMAIN, light.SUPPORT_EFFECT, None) + + trt = trait.ModesTrait( + hass, + State( + "light.living_room", + light.STATE_ON, + attributes={ + light.ATTR_EFFECT_LIST: ["random", "colorloop"], + light.ATTR_EFFECT: "random", + }, + ), + BASIC_CONFIG, + ) + + attribs = trt.sync_attributes() + assert attribs == { + "availableModes": [ + { + "name": "effect", + "name_values": [{"name_synonym": ["effect"], "lang": "en"}], + "settings": [ + { + "setting_name": "random", + "setting_values": [ + {"setting_synonym": ["random"], "lang": "en"} + ], + }, + { + "setting_name": "colorloop", + "setting_values": [ + {"setting_synonym": ["colorloop"], "lang": "en"} + ], + }, + ], + "ordered": False, + } + ] + } + + assert trt.query_attributes() == { + "currentModeSettings": {"effect": "random"}, + "on": True, + } + + assert trt.can_execute( + trait.COMMAND_MODES, params={"updateModeSettings": {"effect": "colorloop"}}, + ) + + calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) + await trt.execute( + trait.COMMAND_MODES, + BASIC_DATA, + {"updateModeSettings": {"effect": "colorloop"}}, + {}, + ) + + assert len(calls) == 1 + assert calls[0].data == { + "entity_id": "light.living_room", + "effect": "colorloop", + } + + async def test_scene_scene(hass): """Test Scene trait support for scene domain.""" assert helpers.get_google_type(scene.DOMAIN, None) is not None diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 2a2e21f77c5..685db475b8c 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -545,13 +545,11 @@ async def test_service_calls(hass): state = hass.states.get("light.ceiling_lights") assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 128 - assert state.attributes[ATTR_EFFECT] == "Random" assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) state = hass.states.get("light.kitchen_lights") assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 128 - assert state.attributes[ATTR_EFFECT] == "Random" assert state.attributes[ATTR_RGB_COLOR] == (42, 255, 255) From 5de21375c04785e623aa1ec6f747d28c115bb69d Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Sun, 9 Aug 2020 04:34:14 +0200 Subject: [PATCH 052/862] Make sure groups are initialized before template sensors (#37766) * Make sure groups are initialized before template sensors This way users may use the `expand` function in templates to expand groups and have HA listen for changes to group members. Fixes #35872 * Patch async_setup_platform instead of async_setup * Cleanup * Use an event to avoid sleep * Update tests/components/template/test_sensor.py Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .../components/template/manifest.json | 3 +- tests/components/template/test_sensor.py | 46 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index 4ad03db22bb..dd2f8d1e0c6 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -3,5 +3,6 @@ "name": "Template", "documentation": "https://www.home-assistant.io/integrations/template", "codeowners": ["@PhracturedBlue", "@tetienne"], - "quality_scale": "internal" + "quality_scale": "internal", + "after_dependencies": ["group"] } diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 8a3a731f953..3899a7b3afe 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1,11 +1,16 @@ """The test for the Template sensor platform.""" +from asyncio import Event +from unittest.mock import patch + +from homeassistant.bootstrap import async_from_config_dict from homeassistant.const import ( + EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import ATTR_COMPONENT, async_setup_component, setup_component from tests.common import assert_setup_component, get_test_home_assistant @@ -438,6 +443,45 @@ class TestTemplateSensor: ) +async def test_creating_sensor_loads_group(hass): + """Test setting up template sensor loads group component first.""" + order = [] + after_dep_event = Event() + + async def async_setup_group(hass, config): + # Make sure group takes longer to load, so that it won't + # be loaded first by chance + await after_dep_event.wait() + + order.append("group") + return True + + async def async_setup_template( + hass, config, async_add_entities, discovery_info=None + ): + order.append("sensor.template") + return True + + async def set_after_dep_event(event): + if event.data[ATTR_COMPONENT] == "sensor": + after_dep_event.set() + + hass.bus.async_listen(EVENT_COMPONENT_LOADED, set_after_dep_event) + + with patch( + "homeassistant.components.group.async_setup", new=async_setup_group, + ), patch( + "homeassistant.components.template.sensor.async_setup_platform", + new=async_setup_template, + ): + await async_from_config_dict( + {"sensor": {"platform": "template", "sensors": {}}, "group": {}}, hass + ) + await hass.async_block_till_done() + + assert order == ["group", "sensor.template"] + + async def test_available_template_with_entities(hass): """Test availability tempalates with values from other entities.""" hass.states.async_set("sensor.availability_sensor", STATE_OFF) From 6856735a1db7d6f3af7dcb31fcc1395cf9558c4f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 8 Aug 2020 21:00:55 -0600 Subject: [PATCH 053/862] Fix missing data for Guardian "AP enabled" binary sensor (#38681) --- homeassistant/components/guardian/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 495f325eb7f..c63d80163bc 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -90,7 +90,7 @@ class GuardianBinarySensor(GuardianEntity, BinarySensorEntity): def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self._kind == SENSOR_KIND_AP_INFO: - self._is_on = self._coordinators[API_WIFI_STATUS].data["ap_enabled"] + self._is_on = self._coordinators[API_WIFI_STATUS].data["station_connected"] self._attrs.update( { ATTR_CONNECTED_CLIENTS: self._coordinators[API_WIFI_STATUS].data[ From f0487b783dafd5f9911afbda89f451f876dccdfb Mon Sep 17 00:00:00 2001 From: On Freund Date: Sun, 9 Aug 2020 08:02:21 +0300 Subject: [PATCH 054/862] Bump pyvolumio to 0.1.1 (#38685) --- homeassistant/components/volumio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/volumio/manifest.json b/homeassistant/components/volumio/manifest.json index 95d84fd7ee6..c5d14859f05 100644 --- a/homeassistant/components/volumio/manifest.json +++ b/homeassistant/components/volumio/manifest.json @@ -5,5 +5,5 @@ "codeowners": ["@OnFreund"], "config_flow": true, "zeroconf": ["_Volumio._tcp.local."], - "requirements": ["pyvolumio==0.1"] + "requirements": ["pyvolumio==0.1.1"] } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index e3576d5a46e..d393b206b6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1831,7 +1831,7 @@ pyvizio==0.1.49 pyvlx==0.2.16 # homeassistant.components.volumio -pyvolumio==0.1 +pyvolumio==0.1.1 # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71b20920545..190975f8ae1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -830,7 +830,7 @@ pyvesync==1.1.0 pyvizio==0.1.49 # homeassistant.components.volumio -pyvolumio==0.1 +pyvolumio==0.1.1 # homeassistant.components.html5 pywebpush==1.9.2 From d659502e35c8b5cb065405a4a6acea859bed0eec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Aug 2020 06:29:46 -0500 Subject: [PATCH 055/862] Update aiohomekit to handle homekit devices that do not send format (#38679) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 9bbaf959012..4d37a38e417 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit[IP]==0.2.45"], + "requirements": ["aiohomekit[IP]==0.2.46"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k"] diff --git a/requirements_all.txt b/requirements_all.txt index d393b206b6e..d3ad3b1a28f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.45 +aiohomekit[IP]==0.2.46 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 190975f8ae1..1714afa4805 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -98,7 +98,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.45 +aiohomekit[IP]==0.2.46 # homeassistant.components.emulated_hue # homeassistant.components.http From ef8e74786f41740f9e644ef8edc6d9cc6652cc94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Aug 2020 06:44:09 -0500 Subject: [PATCH 056/862] Support extracting entities by domain from templates (#38647) --- homeassistant/helpers/template.py | 9 ++++-- tests/helpers/test_template.py | 47 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ef0b578811e..140c233f41e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -46,8 +46,7 @@ _ENVIRONMENT = "template.environment" _RE_NONE_ENTITIES = re.compile(r"distance\(|closest\(", re.I | re.M) _RE_GET_ENTITIES = re.compile( - r"(?:(?:states\.|(?Pis_state|is_state_attr|state_attr|states|expand)" - r"\((?:[\ \'\"]?))(?P[\w]+\.[\w]+)|(?P[\w]+))", + r"(?:(?:(?:states\.|(?Pis_state|is_state_attr|state_attr|states|expand)\((?:[\ \'\"]?))(?P[\w]+\.[\w]+)|states\.(?P[a-z]+)|states\[(?:[\'\"]?)(?P[\w]+))|(?P[\w]+))", re.I | re.M, ) _RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{") @@ -105,6 +104,12 @@ def extract_entities( extraction_final.append(entity.entity_id) extraction_final.append(result.group("entity_id")) + elif result.group("domain_inner") or result.group("domain_outer"): + extraction_final.extend( + hass.states.async_entity_ids( + result.group("domain_inner") or result.group("domain_outer") + ) + ) if ( variables diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 89486129760..fa650b280c6 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1812,6 +1812,53 @@ def test_extract_entities_with_variables(hass): ) +def test_extract_entities_domain_states_inner(hass): + """Test extract entities function by domain.""" + hass.states.async_set("light.switch", "on") + hass.states.async_set("light.switch2", "on") + hass.states.async_set("light.switch3", "off") + + assert set( + template.extract_entities( + hass, + "{{ states['light'] | selectattr('state','eq','on') | list | count > 0 }}", + {}, + ) + ) == {"light.switch", "light.switch2", "light.switch3"} + + +def test_extract_entities_domain_states_outer(hass): + """Test extract entities function by domain.""" + hass.states.async_set("light.switch", "on") + hass.states.async_set("light.switch2", "on") + hass.states.async_set("light.switch3", "off") + + assert set( + template.extract_entities( + hass, + "{{ states.light | selectattr('state','eq','off') | list | count > 0 }}", + {}, + ) + ) == {"light.switch", "light.switch2", "light.switch3"} + + +def test_extract_entities_domain_states_outer_with_group(hass): + """Test extract entities function by domain.""" + hass.states.async_set("light.switch", "on") + hass.states.async_set("light.switch2", "on") + hass.states.async_set("light.switch3", "off") + hass.states.async_set("switch.pool_light", "off") + hass.states.async_set("group.lights", "off", {"entity_id": ["switch.pool_light"]}) + + assert set( + template.extract_entities( + hass, + "{{ states.light | selectattr('entity_id', 'in', state_attr('group.lights', 'entity_id')) }}", + {}, + ) + ) == {"light.switch", "light.switch2", "light.switch3", "group.lights"} + + def test_jinja_namespace(hass): """Test Jinja's namespace command can be used.""" test_template = template.Template( From a6f869aeee67705967a11f54c84f9b4b998e239f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Aug 2020 06:45:16 -0500 Subject: [PATCH 057/862] Improve performance of fetching the state domain (#38653) Pre calc domain when an entity id created to avoid having to call the property method which had to call split_entity_id every time. If there are a lot of zone related automations, async_active_zone can call async_entity_ids frequently which results in 100000s of split_entity_id via the domain property every second. --- homeassistant/core.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index da40c17c411..14699aba33e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -759,6 +759,7 @@ class State: last_changed: last time the state was changed, not the attributes. last_updated: last time this object was updated. context: Context in which it was created + domain: Domain of this state. """ __slots__ = [ @@ -768,6 +769,7 @@ class State: "last_changed", "last_updated", "context", + "domain", ] def __init__( @@ -801,11 +803,7 @@ class State: self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated self.context = context or Context() - - @property - def domain(self) -> str: - """Domain of this state.""" - return split_entity_id(self.entity_id)[0] + self.domain = split_entity_id(self.entity_id)[0] @property def object_id(self) -> str: From 53b729a0d17cd4d1621a2185c29d082d6d00184c Mon Sep 17 00:00:00 2001 From: Justin Paupore Date: Sun, 9 Aug 2020 05:03:53 -0700 Subject: [PATCH 058/862] Support muting and relative-volume media_players in Google Assistant (#38651) Support the action.devices.commands.mute intent to mute and unmute media_players that declare support for mute/unmute. For media players with support for volume up/down, but no support for setting the volume to a specific number, allow use of the action.devices.commands.relativeMute intent to control volume up/down. This will improve support for IR blasters and other open-loop media_player integrations. --- .../components/google_assistant/trait.py | 94 +++++++++--- .../components/google_assistant/test_trait.py | 135 +++++++++++++++--- 2 files changed, 190 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index b347294f275..f7aa2d43663 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -121,6 +121,7 @@ COMMAND_PREVIOUS_INPUT = f"{PREFIX_COMMANDS}PreviousInput" COMMAND_OPENCLOSE = f"{PREFIX_COMMANDS}OpenClose" COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume" COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative" +COMMAND_MUTE = f"{PREFIX_COMMANDS}mute" COMMAND_ARMDISARM = f"{PREFIX_COMMANDS}ArmDisarm" COMMAND_MEDIA_NEXT = f"{PREFIX_COMMANDS}mediaNext" COMMAND_MEDIA_PAUSE = f"{PREFIX_COMMANDS}mediaPause" @@ -1627,75 +1628,132 @@ class OpenCloseTrait(_Trait): @register_trait class VolumeTrait(_Trait): - """Trait to control brightness of a device. + """Trait to control volume of a device. https://developers.google.com/actions/smarthome/traits/volume """ name = TRAIT_VOLUME - commands = [COMMAND_SET_VOLUME, COMMAND_VOLUME_RELATIVE] + commands = [COMMAND_SET_VOLUME, COMMAND_VOLUME_RELATIVE, COMMAND_MUTE] @staticmethod def supported(domain, features, device_class): - """Test if state is supported.""" + """Test if trait is supported.""" if domain == media_player.DOMAIN: - return features & media_player.SUPPORT_VOLUME_SET + return features & ( + media_player.SUPPORT_VOLUME_SET | media_player.SUPPORT_VOLUME_STEP + ) return False def sync_attributes(self): - """Return brightness attributes for a sync request.""" - return {} + """Return volume attributes for a sync request.""" + features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + return { + "volumeCanMuteAndUnmute": bool(features & media_player.SUPPORT_VOLUME_MUTE), + "commandOnlyVolume": self.state.attributes.get(ATTR_ASSUMED_STATE, False), + # Volume amounts in SET_VOLUME and VOLUME_RELATIVE are on a scale + # from 0 to this value. + "volumeMaxLevel": 100, + # Default change for queries like "Hey Google, volume up". + # 10% corresponds to the default behavior for the + # media_player.volume{up,down} services. + "levelStepSize": 10, + } def query_attributes(self): - """Return brightness query attributes.""" + """Return volume query attributes.""" response = {} level = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) - muted = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED) if level is not None: # Convert 0.0-1.0 to 0-100 response["currentVolume"] = int(level * 100) + + muted = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED) + if muted is not None: response["isMuted"] = bool(muted) return response - async def _execute_set_volume(self, data, params): - level = params["volumeLevel"] - + async def _set_volume_absolute(self, data, level): await self.hass.services.async_call( media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, { ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: level / 100, + media_player.ATTR_MEDIA_VOLUME_LEVEL: level, }, blocking=True, context=data.context, ) + async def _execute_set_volume(self, data, params): + level = max(0, min(100, params["volumeLevel"])) + + if not ( + self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + & media_player.SUPPORT_VOLUME_SET + ): + raise SmartHomeError(ERR_NOT_SUPPORTED, "Command not supported") + + await self._set_volume_absolute(data, level / 100) + async def _execute_volume_relative(self, data, params): - # This could also support up/down commands using relativeSteps - relative = params["volumeRelativeLevel"] - current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + relative = params["relativeSteps"] + features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if features & media_player.SUPPORT_VOLUME_SET: + current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + target = max(0.0, min(1.0, current + relative / 100)) + + await self._set_volume_absolute(data, target) + + elif features & media_player.SUPPORT_VOLUME_STEP: + svc = media_player.SERVICE_VOLUME_UP + if relative < 0: + svc = media_player.SERVICE_VOLUME_DOWN + relative = -relative + + for i in range(relative): + await self.hass.services.async_call( + media_player.DOMAIN, + svc, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) + else: + raise SmartHomeError(ERR_NOT_SUPPORTED, "Command not supported") + + async def _execute_mute(self, data, params): + mute = params["mute"] + + if not ( + self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + & media_player.SUPPORT_VOLUME_MUTE + ): + raise SmartHomeError(ERR_NOT_SUPPORTED, "Command not supported") await self.hass.services.async_call( media_player.DOMAIN, - media_player.SERVICE_VOLUME_SET, + media_player.SERVICE_VOLUME_MUTE, { ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, + media_player.ATTR_MEDIA_VOLUME_MUTED: mute, }, blocking=True, context=data.context, ) async def execute(self, command, data, params, challenge): - """Execute a brightness command.""" + """Execute a volume command.""" if command == COMMAND_SET_VOLUME: await self._execute_set_volume(data, params) elif command == COMMAND_VOLUME_RELATIVE: await self._execute_volume_relative(data, params) + elif command == COMMAND_MUTE: + await self._execute_mute(data, params) else: raise SmartHomeError(ERR_NOT_SUPPORTED, "Command not supported") diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 9d821b357ca..a8f6b58fc46 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -2003,9 +2003,7 @@ async def test_volume_media_player(hass): """Test volume trait support for media player domain.""" assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.VolumeTrait.supported( - media_player.DOMAIN, - media_player.SUPPORT_VOLUME_SET | media_player.SUPPORT_VOLUME_MUTE, - None, + media_player.DOMAIN, media_player.SUPPORT_VOLUME_SET, None, ) trt = trait.VolumeTrait( @@ -2014,16 +2012,21 @@ async def test_volume_media_player(hass): "media_player.bla", media_player.STATE_PLAYING, { + ATTR_SUPPORTED_FEATURES: media_player.SUPPORT_VOLUME_SET, media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.3, - media_player.ATTR_MEDIA_VOLUME_MUTED: False, }, ), BASIC_CONFIG, ) - assert trt.sync_attributes() == {} + assert trt.sync_attributes() == { + "volumeMaxLevel": 100, + "levelStepSize": 10, + "volumeCanMuteAndUnmute": False, + "commandOnlyVolume": False, + } - assert trt.query_attributes() == {"currentVolume": 30, "isMuted": False} + assert trt.query_attributes() == {"currentVolume": 30} calls = async_mock_service( hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET @@ -2035,40 +2038,130 @@ async def test_volume_media_player(hass): media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.6, } + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET + ) + await trt.execute( + trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"relativeSteps": 10}, {} + ) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: "media_player.bla", + media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.4, + } + async def test_volume_media_player_relative(hass): - """Test volume trait support for media player domain.""" + """Test volume trait support for relative-volume-only media players.""" + assert trait.VolumeTrait.supported( + media_player.DOMAIN, media_player.SUPPORT_VOLUME_STEP, None, + ) trt = trait.VolumeTrait( hass, State( "media_player.bla", media_player.STATE_PLAYING, { - media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.3, + ATTR_ASSUMED_STATE: True, + ATTR_SUPPORTED_FEATURES: media_player.SUPPORT_VOLUME_STEP, + }, + ), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == { + "volumeMaxLevel": 100, + "levelStepSize": 10, + "volumeCanMuteAndUnmute": False, + "commandOnlyVolume": True, + } + + assert trt.query_attributes() == {} + + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_UP + ) + + await trt.execute( + trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"relativeSteps": 10}, {}, + ) + assert len(calls) == 10 + for call in calls: + assert call.data == { + ATTR_ENTITY_ID: "media_player.bla", + } + + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_DOWN + ) + await trt.execute( + trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"relativeSteps": -10}, {}, + ) + assert len(calls) == 10 + for call in calls: + assert call.data == { + ATTR_ENTITY_ID: "media_player.bla", + } + + with pytest.raises(SmartHomeError): + await trt.execute(trait.COMMAND_SET_VOLUME, BASIC_DATA, {"volumeLevel": 42}, {}) + + with pytest.raises(SmartHomeError): + await trt.execute(trait.COMMAND_MUTE, BASIC_DATA, {"mute": True}, {}) + + +async def test_media_player_mute(hass): + """Test volume trait support for muting.""" + assert trait.VolumeTrait.supported( + media_player.DOMAIN, + media_player.SUPPORT_VOLUME_STEP | media_player.SUPPORT_VOLUME_MUTE, + None, + ) + trt = trait.VolumeTrait( + hass, + State( + "media_player.bla", + media_player.STATE_PLAYING, + { + ATTR_SUPPORTED_FEATURES: ( + media_player.SUPPORT_VOLUME_STEP | media_player.SUPPORT_VOLUME_MUTE + ), media_player.ATTR_MEDIA_VOLUME_MUTED: False, }, ), BASIC_CONFIG, ) - assert trt.sync_attributes() == {} + assert trt.sync_attributes() == { + "volumeMaxLevel": 100, + "levelStepSize": 10, + "volumeCanMuteAndUnmute": True, + "commandOnlyVolume": False, + } + assert trt.query_attributes() == {"isMuted": False} - assert trt.query_attributes() == {"currentVolume": 30, "isMuted": False} - - calls = async_mock_service( - hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET + mute_calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_MUTE ) - await trt.execute( - trait.COMMAND_VOLUME_RELATIVE, - BASIC_DATA, - {"volumeRelativeLevel": 20, "relativeSteps": 2}, - {}, + trait.COMMAND_MUTE, BASIC_DATA, {"mute": True}, {}, ) - assert len(calls) == 1 - assert calls[0].data == { + assert len(mute_calls) == 1 + assert mute_calls[0].data == { ATTR_ENTITY_ID: "media_player.bla", - media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.5, + media_player.ATTR_MEDIA_VOLUME_MUTED: True, + } + + unmute_calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_MUTE + ) + await trt.execute( + trait.COMMAND_MUTE, BASIC_DATA, {"mute": False}, {}, + ) + assert len(unmute_calls) == 1 + assert unmute_calls[0].data == { + ATTR_ENTITY_ID: "media_player.bla", + media_player.ATTR_MEDIA_VOLUME_MUTED: False, } From 20710d8605ff79a4fbce47e78863995b97e2b9a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 9 Aug 2020 14:07:31 +0200 Subject: [PATCH 059/862] Add current request context to get_url helper (#38602) --- homeassistant/components/http/__init__.py | 8 + .../components/http/request_context.py | 20 +++ homeassistant/helpers/network.py | 46 +++++- tests/components/http/test_request_context.py | 33 ++++ tests/helpers/test_network.py | 153 ++++++++++++++++++ 5 files changed, 252 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/http/request_context.py create mode 100644 tests/components/http/test_request_context.py diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 75f13caa6aa..36e44508928 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -1,4 +1,5 @@ """Support to serve the Home Assistant API as WSGI application.""" +from contextvars import ContextVar from ipaddress import ip_network import logging import os @@ -28,6 +29,7 @@ from .ban import setup_bans from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER, KEY_REAL_IP # noqa: F401 from .cors import setup_cors from .real_ip import setup_real_ip +from .request_context import setup_request_context from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa: F401 from .web_runner import HomeAssistantTCPSite @@ -295,6 +297,7 @@ class HomeAssistantHTTP: app[KEY_HASS] = hass # This order matters + setup_request_context(app, current_request) setup_real_ip(app, use_x_forwarded_for, trusted_proxies) if is_ban_enabled: @@ -447,3 +450,8 @@ async def start_http_server_and_save_config( ] await store.async_save(conf) + + +current_request: ContextVar[Optional[web.Request]] = ContextVar( + "current_request", default=None +) diff --git a/homeassistant/components/http/request_context.py b/homeassistant/components/http/request_context.py new file mode 100644 index 00000000000..23a85214c3f --- /dev/null +++ b/homeassistant/components/http/request_context.py @@ -0,0 +1,20 @@ +"""Middleware to set the request context.""" + +from aiohttp.web import middleware + +from homeassistant.core import callback + +# mypy: allow-untyped-defs + + +@callback +def setup_request_context(app, context): + """Create request context middleware for the app.""" + + @middleware + async def request_context_middleware(request, handler): + """Request context middleware.""" + context.set(request) + return await handler(request) + + app.middlewares.append(request_context_middleware) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index cebe0318496..471cabd0032 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,9 +1,10 @@ """Network helpers.""" from ipaddress import ip_address -from typing import cast +from typing import Optional, cast import yarl +from homeassistant.components.http import current_request from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass @@ -27,6 +28,7 @@ class NoURLAvailableError(HomeAssistantError): def get_url( hass: HomeAssistant, *, + require_current_request: bool = False, require_ssl: bool = False, require_standard_port: bool = False, allow_internal: bool = True, @@ -37,6 +39,9 @@ def get_url( prefer_cloud: bool = False, ) -> str: """Get a URL to this instance.""" + if require_current_request and current_request.get() is None: + raise NoURLAvailableError + order = [TYPE_URL_INTERNAL, TYPE_URL_EXTERNAL] if prefer_external: order.reverse() @@ -49,6 +54,7 @@ def get_url( return _get_internal_url( hass, allow_ip=allow_ip, + require_current_request=require_current_request, require_ssl=require_ssl, require_standard_port=require_standard_port, ) @@ -62,6 +68,7 @@ def get_url( allow_cloud=allow_cloud, allow_ip=allow_ip, prefer_cloud=prefer_cloud, + require_current_request=require_current_request, require_ssl=require_ssl, require_standard_port=require_standard_port, ) @@ -72,11 +79,20 @@ def get_url( raise NoURLAvailableError +def _get_request_host() -> Optional[str]: + """Get the host address of the current request.""" + request = current_request.get() + if request is None: + raise NoURLAvailableError + return yarl.URL(request.url).host + + @bind_hass def _get_internal_url( hass: HomeAssistant, *, allow_ip: bool = True, + require_current_request: bool = False, require_ssl: bool = False, require_standard_port: bool = False, ) -> str: @@ -84,7 +100,8 @@ def _get_internal_url( if hass.config.internal_url: internal_url = yarl.URL(hass.config.internal_url) if ( - (not require_ssl or internal_url.scheme == "https") + (not require_current_request or internal_url.host == _get_request_host()) + and (not require_ssl or internal_url.scheme == "https") and (not require_standard_port or internal_url.is_default_port()) and (allow_ip or not is_ip_address(str(internal_url.host))) ): @@ -96,6 +113,7 @@ def _get_internal_url( hass, internal=True, allow_ip=allow_ip, + require_current_request=require_current_request, require_ssl=require_ssl, require_standard_port=require_standard_port, ) @@ -109,8 +127,10 @@ def _get_internal_url( ip_url = yarl.URL.build( scheme="http", host=hass.config.api.local_ip, port=hass.config.api.port ) - if not is_loopback(ip_address(ip_url.host)) and ( - not require_standard_port or ip_url.is_default_port() + if ( + not is_loopback(ip_address(ip_url.host)) + and (not require_current_request or ip_url.host == _get_request_host()) + and (not require_standard_port or ip_url.is_default_port()) ): return normalize_url(str(ip_url)) @@ -124,6 +144,7 @@ def _get_external_url( allow_cloud: bool = True, allow_ip: bool = True, prefer_cloud: bool = False, + require_current_request: bool = False, require_ssl: bool = False, require_standard_port: bool = False, ) -> str: @@ -138,6 +159,9 @@ def _get_external_url( external_url = yarl.URL(hass.config.external_url) if ( (allow_ip or not is_ip_address(str(external_url.host))) + and ( + not require_current_request or external_url.host == _get_request_host() + ) and (not require_standard_port or external_url.is_default_port()) and ( not require_ssl @@ -153,6 +177,7 @@ def _get_external_url( return _get_deprecated_base_url( hass, allow_ip=allow_ip, + require_current_request=require_current_request, require_ssl=require_ssl, require_standard_port=require_standard_port, ) @@ -161,7 +186,7 @@ def _get_external_url( if allow_cloud: try: - return _get_cloud_url(hass) + return _get_cloud_url(hass, require_current_request=require_current_request) except NoURLAvailableError: pass @@ -169,13 +194,16 @@ def _get_external_url( @bind_hass -def _get_cloud_url(hass: HomeAssistant) -> str: +def _get_cloud_url(hass: HomeAssistant, require_current_request: bool = False) -> str: """Get external Home Assistant Cloud URL of this instance.""" if "cloud" in hass.config.components: try: - return cast(str, hass.components.cloud.async_remote_ui_url()) + cloud_url = yarl.URL(cast(str, hass.components.cloud.async_remote_ui_url())) except hass.components.cloud.CloudNotAvailable: - pass + raise NoURLAvailableError + + if not require_current_request or cloud_url.host == _get_request_host(): + return normalize_url(str(cloud_url)) raise NoURLAvailableError @@ -186,6 +214,7 @@ def _get_deprecated_base_url( *, internal: bool = False, allow_ip: bool = True, + require_current_request: bool = False, require_ssl: bool = False, require_standard_port: bool = False, ) -> str: @@ -197,6 +226,7 @@ def _get_deprecated_base_url( # Rules that apply to both internal and external if ( (allow_ip or not is_ip_address(str(base_url.host))) + and (not require_current_request or base_url.host == _get_request_host()) and (not require_ssl or base_url.scheme == "https") and (not require_standard_port or base_url.is_default_port()) ): diff --git a/tests/components/http/test_request_context.py b/tests/components/http/test_request_context.py new file mode 100644 index 00000000000..f511b860dca --- /dev/null +++ b/tests/components/http/test_request_context.py @@ -0,0 +1,33 @@ +"""Test request context middleware.""" +from contextvars import ContextVar + +from aiohttp import web + +from homeassistant.components.http.request_context import setup_request_context + + +async def test_request_context_middleware(aiohttp_client): + """Test that request context is set from middleware.""" + context = ContextVar("request", default=None) + app = web.Application() + + async def mock_handler(request): + """Return the real IP as text.""" + request_context = context.get() + assert request_context + assert request_context == request + + return web.Response(text="hi!") + + app.router.add_get("/", mock_handler) + setup_request_context(app, context) + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get("/") + assert resp.status == 200 + + text = await resp.text() + assert text == "hi!" + + # We are outside of the context here, should be None + assert context.get() is None diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index f6665b054e7..1754511d95c 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -10,6 +10,7 @@ from homeassistant.helpers.network import ( _get_deprecated_base_url, _get_external_url, _get_internal_url, + _get_request_host, get_url, ) @@ -20,6 +21,9 @@ async def test_get_url_internal(hass: HomeAssistant): """Test getting an instance URL when the user has set an internal URL.""" assert hass.config.internal_url is None + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_current_request=True) + # Test with internal URL: http://example.local:8123 await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -35,6 +39,31 @@ async def test_get_url_internal(hass: HomeAssistant): with pytest.raises(NoURLAvailableError): _get_internal_url(hass, require_ssl=True) + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_current_request=True) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="example.local" + ): + assert ( + _get_internal_url(hass, require_current_request=True) + == "http://example.local:8123" + ) + + with pytest.raises(NoURLAvailableError): + _get_internal_url( + hass, require_current_request=True, require_standard_port=True + ) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_current_request=True, require_ssl=True) + + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="no_match.example.local", + ), pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_current_request=True) + # Test with internal URL: https://example.local:8123 await async_process_ha_core_config( hass, {"internal_url": "https://example.local:8123"}, @@ -104,6 +133,25 @@ async def test_get_url_internal(hass: HomeAssistant): with pytest.raises(NoURLAvailableError): _get_internal_url(hass, allow_ip=False) + with patch( + "homeassistant.helpers.network._get_request_host", return_value="192.168.0.1" + ): + assert ( + _get_internal_url(hass, require_current_request=True) + == "http://192.168.0.1:8123" + ) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_current_request=True, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_internal_url( + hass, require_current_request=True, require_standard_port=True + ) + + with pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_current_request=True, require_ssl=True) + async def test_get_url_internal_fallback(hass: HomeAssistant): """Test getting an instance URL when the user has not set an internal URL.""" @@ -171,6 +219,9 @@ async def test_get_url_external(hass: HomeAssistant): """Test getting an instance URL when the user has set an external URL.""" assert hass.config.external_url is None + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_current_request=True) + # Test with external URL: http://example.com:8123 await async_process_ha_core_config( hass, {"external_url": "http://example.com:8123"}, @@ -188,6 +239,31 @@ async def test_get_url_external(hass: HomeAssistant): with pytest.raises(NoURLAvailableError): _get_external_url(hass, require_ssl=True) + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_current_request=True) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="example.com" + ): + assert ( + _get_external_url(hass, require_current_request=True) + == "http://example.com:8123" + ) + + with pytest.raises(NoURLAvailableError): + _get_external_url( + hass, require_current_request=True, require_standard_port=True + ) + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_current_request=True, require_ssl=True) + + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="no_match.example.com", + ), pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_current_request=True) + # Test with external URL: http://example.com:80/ await async_process_ha_core_config( hass, {"external_url": "http://example.com:80/"}, @@ -245,6 +321,20 @@ async def test_get_url_external(hass: HomeAssistant): with pytest.raises(NoURLAvailableError): _get_external_url(hass, require_ssl=True) + with patch( + "homeassistant.helpers.network._get_request_host", return_value="192.168.0.1" + ): + assert ( + _get_external_url(hass, require_current_request=True) + == "https://192.168.0.1" + ) + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_current_request=True, allow_ip=False) + + with pytest.raises(NoURLAvailableError): + _get_external_url(hass, require_current_request=True, require_ssl=True) + async def test_get_cloud_url(hass: HomeAssistant): """Test getting an instance URL when the user has set an external URL.""" @@ -258,6 +348,24 @@ async def test_get_cloud_url(hass: HomeAssistant): ): assert _get_cloud_url(hass) == "https://example.nabu.casa" + with pytest.raises(NoURLAvailableError): + _get_cloud_url(hass, require_current_request=True) + + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="example.nabu.casa", + ): + assert ( + _get_cloud_url(hass, require_current_request=True) + == "https://example.nabu.casa" + ) + + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="no_match.nabu.casa", + ), pytest.raises(NoURLAvailableError): + _get_cloud_url(hass, require_current_request=True) + with patch.object( hass.components.cloud, "async_remote_ui_url", @@ -372,6 +480,51 @@ async def test_get_url(hass: HomeAssistant): with pytest.raises(NoURLAvailableError): get_url(hass, allow_external=False, allow_internal=False) + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="example.com" + ), patch("homeassistant.helpers.network.current_request"): + assert get_url(hass, require_current_request=True) == "https://example.com" + assert ( + get_url(hass, require_current_request=True, require_ssl=True) + == "https://example.com" + ) + + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, allow_external=False) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="example.local" + ), patch("homeassistant.helpers.network.current_request"): + assert get_url(hass, require_current_request=True) == "http://example.local" + + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, allow_internal=False) + + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, require_ssl=True) + + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="no_match.example.com", + ), pytest.raises(NoURLAvailableError): + _get_internal_url(hass, require_current_request=True) + + +async def test_get_request_host(hass: HomeAssistant): + """Test getting the host of the current web request from the request context.""" + with pytest.raises(NoURLAvailableError): + _get_request_host() + + with patch("homeassistant.helpers.network.current_request") as mock_request_context: + mock_request = Mock() + mock_request.url = "http://example.com:8123/test/request" + mock_request_context.get = Mock(return_value=mock_request) + + assert _get_request_host() == "example.com" + async def test_get_deprecated_base_url_internal(hass: HomeAssistant): """Test getting an internal instance URL from the deprecated base_url.""" From ac1f431f91da6a5650478820427c46b8656149d5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 9 Aug 2020 14:10:27 +0200 Subject: [PATCH 060/862] Bump updater timeout (#38690) --- homeassistant/components/updater/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 59f858f7cf4..d90efe132f6 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -134,7 +134,7 @@ async def get_newest_version(hass, huuid, include_components): session = async_get_clientsession(hass) - with async_timeout.timeout(15): + with async_timeout.timeout(30): req = await session.post(UPDATER_URL, json=info_object) _LOGGER.info( From ca3842b1502eb3e83c72fcf040b60f36d35464c4 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 9 Aug 2020 07:16:39 -0700 Subject: [PATCH 061/862] Address requested code changes in Tesla (#38680) * Address requested code changes * Address ternary operator in sensor --- homeassistant/components/tesla/__init__.py | 10 ++++++---- .../components/tesla/device_tracker.py | 17 +++++++---------- homeassistant/components/tesla/sensor.py | 19 +++---------------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 1dc6bce01de..fefe980fbd1 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -251,17 +251,19 @@ class TeslaDevice(Entity): """Initialise the Tesla device.""" self.tesla_device = tesla_device self.coordinator = coordinator + self._name = self.tesla_device.name + self._unique_id = slugify(self.tesla_device.uniq_name) self._attributes = self.tesla_device.attrs.copy() @property def name(self): """Return the name of the device.""" - return self.tesla_device.name + return self._name @property def unique_id(self) -> str: """Return a unique ID.""" - return slugify(self.tesla_device.uniq_name) + return self._unique_id @property def icon(self): @@ -312,12 +314,12 @@ class TeslaDevice(Entity): """Update the state of the device.""" _LOGGER.debug("Updating state for: %s", self.name) await self.coordinator.async_request_refresh() - self.refresh() + @callback def refresh(self) -> None: """Refresh the state of the device. This assumes the coordinator has updated the controller. """ self.tesla_device.refresh() - self.schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index ce5ea5a2a8a..c3c82a708bf 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -26,11 +26,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TeslaDeviceEntity(TeslaDevice, TrackerEntity): """A class representing a Tesla device.""" - def __init__(self, tesla_device, coordinator): - """Initialize the Tesla device scanner.""" - super().__init__(tesla_device, coordinator) - self._attributes = {"trackr_id": self.unique_id} - @property def latitude(self) -> Optional[float]: """Return latitude value of the device.""" @@ -54,9 +49,11 @@ class TeslaDeviceEntity(TeslaDevice, TrackerEntity): attr = super().device_state_attributes.copy() location = self.tesla_device.get_location() if location: - self._attributes = { - "trackr_id": self.unique_id, - "heading": location["heading"], - "speed": location["speed"], - } + attr.update( + { + "trackr_id": self.unique_id, + "heading": location["heading"], + "speed": location["speed"], + } + ) return attr diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 93cc03cd718..50be1edc87d 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -37,22 +37,9 @@ class TeslaSensor(TeslaDevice, Entity): """Initialize of the sensor.""" super().__init__(tesla_device, coordinator) self.type = sensor_type - - @property - def name(self) -> str: - """Return the name of the device.""" - return ( - self.tesla_device.name - if not self.type - else f"{self.tesla_device.name} ({self.type})" - ) - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return ( - super().unique_id if not self.type else f"{super().unique_id}_{self.type}" - ) + if self.type: + self._name = f"{super().name} ({self.type})" + self._unique_id = f"{super().unique_id}_{self.type}" @property def state(self) -> Optional[float]: From a7e19c8896b5fa7587e003edd33fa5034ebeab8e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 9 Aug 2020 16:21:36 +0200 Subject: [PATCH 062/862] Improve tests for AccuWeather integration (#38621) * Add more tests * Add tests for sensor platform * Add more tests * More tests * Simplify parsing of attributes * Change Quality scale to platinum * Patch the library in the manual update entity test * Add unsupported condition icon test * Do not patch _async_get_data * Apply suggestions from code review * Update config_flow.py * Apply suggestions from code review * Apply suggestions from code review * Apply suggestions from code review * Update tests/components/accuweather/test_weather.py * Apply suggestions from code review * Add return_value Co-authored-by: Chris Talkington --- .coveragerc | 4 - .../components/accuweather/config_flow.py | 2 +- .../components/accuweather/manifest.json | 3 +- .../components/accuweather/sensor.py | 6 +- tests/components/accuweather/__init__.py | 47 + tests/components/accuweather/test_init.py | 59 ++ tests/components/accuweather/test_sensor.py | 598 +++++++++++ tests/components/accuweather/test_weather.py | 155 +++ tests/fixtures/accuweather/forecast_data.json | 981 ++++++++++++++++++ 9 files changed, 1844 insertions(+), 11 deletions(-) create mode 100644 tests/components/accuweather/test_init.py create mode 100644 tests/components/accuweather/test_sensor.py create mode 100644 tests/components/accuweather/test_weather.py create mode 100644 tests/fixtures/accuweather/forecast_data.json diff --git a/.coveragerc b/.coveragerc index b716874c679..6b4133bf476 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,10 +8,6 @@ omit = homeassistant/scripts/*.py # omit pieces of code that rely on external devices being present - homeassistant/components/accuweather/__init__.py - homeassistant/components/accuweather/const.py - homeassistant/components/accuweather/sensor.py - homeassistant/components/accuweather/weather.py homeassistant/components/acer_projector/switch.py homeassistant/components/actiontec/device_tracker.py homeassistant/components/acmeda/__init__.py diff --git a/homeassistant/components/accuweather/config_flow.py b/homeassistant/components/accuweather/config_flow.py index d50a2ac406b..03d6f40181c 100644 --- a/homeassistant/components/accuweather/config_flow.py +++ b/homeassistant/components/accuweather/config_flow.py @@ -34,7 +34,7 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: websession = async_get_clientsession(self.hass) try: - with timeout(10): + async with timeout(10): accuweather = AccuWeather( user_input[CONF_API_KEY], websession, diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 4e54d937dee..dd039717468 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/accuweather/", "requirements": ["accuweather==0.0.9"], "codeowners": ["@bieniu"], - "config_flow": true + "config_flow": true, + "quality_scale": "platinum" } diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 878f387c35c..9ab44a318a9 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -154,11 +154,7 @@ class AccuWeatherSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" if self.forecast_day is not None: - if self.kind == "WindGustDay": - self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][ - self.forecast_day - ][self.kind]["Direction"]["English"] - elif self.kind == "WindGustNight": + if self.kind in ["WindGustDay", "WindGustNight"]: self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][ self.forecast_day ][self.kind]["Direction"]["English"] diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index 97ae531ddd0..28e53d1e2fc 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -1 +1,48 @@ """Tests for AccuWeather.""" +import json + +from homeassistant.components.accuweather.const import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry, load_fixture + + +async def init_integration( + hass, forecast=False, unsupported_icon=False +) -> MockConfigEntry: + """Set up the AccuWeather integration in Home Assistant.""" + options = {} + if forecast: + options["forecast"] = True + + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="0123456", + data={ + "api_key": "32-character-string-1234567890qw", + "latitude": 55.55, + "longitude": 122.12, + "name": "Home", + }, + options=options, + ) + + current = json.loads(load_fixture("accuweather/current_conditions_data.json")) + forecast = json.loads(load_fixture("accuweather/forecast_data.json")) + + if unsupported_icon: + current["WeatherIcon"] = 999 + + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ), patch( + "homeassistant.components.accuweather.AccuWeather.async_get_forecast", + return_value=forecast, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py new file mode 100644 index 00000000000..0a54132fd68 --- /dev/null +++ b/tests/components/accuweather/test_init.py @@ -0,0 +1,59 @@ +"""Test init of AccuWeather integration.""" +from homeassistant.components.accuweather.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import STATE_UNAVAILABLE + +from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.components.accuweather import init_integration + + +async def test_async_setup_entry(hass): + """Test a successful setup entry.""" + await init_integration(hass) + + state = hass.states.get("weather.home") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "sunny" + + +async def test_config_not_ready(hass): + """Test for setup failure if connection to AccuWeather is missing.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="0123456", + data={ + "api_key": "32-character-string-1234567890qw", + "latitude": 55.55, + "longitude": 122.12, + "name": "Home", + }, + ) + + with patch( + "homeassistant.components.accuweather.AccuWeather._async_get_data", + side_effect=ConnectionError(), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py new file mode 100644 index 00000000000..26c7f0d9ab6 --- /dev/null +++ b/tests/components/accuweather/test_sensor.py @@ -0,0 +1,598 @@ +"""Test sensor of AccuWeather integration.""" +from datetime import timedelta +import json + +from homeassistant.components.accuweather.const import ( + ATTRIBUTION, + CONCENTRATION_PARTS_PER_CUBIC_METER, + DOMAIN, + LENGTH_MILIMETERS, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_TEMPERATURE, + LENGTH_METERS, + SPEED_KILOMETERS_PER_HOUR, + STATE_UNAVAILABLE, + TEMP_CELSIUS, + TIME_HOURS, + UNIT_PERCENTAGE, + UV_INDEX, +) +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.async_mock import patch +from tests.common import async_fire_time_changed, load_fixture +from tests.components.accuweather import init_integration + + +async def test_sensor_without_forecast(hass): + """Test states of the sensor without forecast.""" + await init_integration(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state == "3200" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_METERS + + entry = registry.async_get("sensor.home_cloud_ceiling") + assert entry + assert entry.unique_id == "0123456-ceiling" + + state = hass.states.get("sensor.home_precipitation") + assert state + assert state.state == "0.0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILIMETERS + assert state.attributes.get(ATTR_ICON) == "mdi:weather-rainy" + assert state.attributes.get("type") is None + + entry = registry.async_get("sensor.home_precipitation") + assert entry + assert entry.unique_id == "0123456-precipitation" + + state = hass.states.get("sensor.home_pressure_tendency") + assert state + assert state.state == "falling" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_ICON) == "mdi:gauge" + assert state.attributes.get(ATTR_DEVICE_CLASS) == "accuweather__pressure_tendency" + + entry = registry.async_get("sensor.home_pressure_tendency") + assert entry + assert entry.unique_id == "0123456-pressuretendency" + + state = hass.states.get("sensor.home_realfeel_temperature") + assert state + assert state.state == "25.1" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_realfeel_temperature") + assert entry + assert entry.unique_id == "0123456-realfeeltemperature" + + state = hass.states.get("sensor.home_uv_index") + assert state + assert state.state == "6" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX + assert state.attributes.get("level") == "High" + + entry = registry.async_get("sensor.home_uv_index") + assert entry + assert entry.unique_id == "0123456-uvindex" + + +async def test_sensor_with_forecast(hass): + """Test states of the sensor with forecast.""" + await init_integration(hass, forecast=True) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.home_hours_of_sun_0d") + assert state + assert state.state == "7.2" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_ICON) == "mdi:weather-partly-cloudy" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_HOURS + + entry = registry.async_get("sensor.home_hours_of_sun_0d") + assert entry + assert entry.unique_id == "0123456-hoursofsun-0" + + state = hass.states.get("sensor.home_realfeel_temperature_max_0d") + assert state + assert state.state == "29.8" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_realfeel_temperature_max_0d") + assert entry + + state = hass.states.get("sensor.home_realfeel_temperature_min_0d") + assert state + assert state.state == "15.1" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_realfeel_temperature_min_0d") + assert entry + assert entry.unique_id == "0123456-realfeeltemperaturemin-0" + + state = hass.states.get("sensor.home_thunderstorm_probability_day_0d") + assert state + assert state.state == "40" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + + entry = registry.async_get("sensor.home_thunderstorm_probability_day_0d") + assert entry + assert entry.unique_id == "0123456-thunderstormprobabilityday-0" + + state = hass.states.get("sensor.home_thunderstorm_probability_night_0d") + assert state + assert state.state == "40" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + + entry = registry.async_get("sensor.home_thunderstorm_probability_night_0d") + assert entry + assert entry.unique_id == "0123456-thunderstormprobabilitynight-0" + + state = hass.states.get("sensor.home_uv_index_0d") + assert state + assert state.state == "5" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_ICON) == "mdi:weather-sunny" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UV_INDEX + assert state.attributes.get("level") == "Moderate" + + entry = registry.async_get("sensor.home_uv_index_0d") + assert entry + assert entry.unique_id == "0123456-uvindex-0" + + +async def test_sensor_disabled(hass): + """Test sensor disabled by default.""" + await init_integration(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + entry = registry.async_get("sensor.home_apparent_temperature") + assert entry + assert entry.unique_id == "0123456-apparenttemperature" + assert entry.disabled + assert entry.disabled_by == "integration" + + # Test enabling entity + updated_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def test_sensor_enabled_without_forecast(hass): + """Test enabling an advanced sensor.""" + registry = await hass.helpers.entity_registry.async_get_registry() + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-apparenttemperature", + suggested_object_id="home_apparent_temperature", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-cloudcover", + suggested_object_id="home_cloud_cover", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-dewpoint", + suggested_object_id="home_dew_point", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-realfeeltemperatureshade", + suggested_object_id="home_realfeel_temperature_shade", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-wetbulbtemperature", + suggested_object_id="home_wet_bulb_temperature", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-windchilltemperature", + suggested_object_id="home_wind_chill_temperature", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-windgust", + suggested_object_id="home_wind_gust", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-cloudcoverday-0", + suggested_object_id="home_cloud_cover_day_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-cloudcovernight-0", + suggested_object_id="home_cloud_cover_night_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-grass-0", + suggested_object_id="home_grass_pollen_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-mold-0", + suggested_object_id="home_mold_pollen_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-ozone-0", + suggested_object_id="home_ozone_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-ragweed-0", + suggested_object_id="home_ragweed_pollen_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-realfeeltemperatureshademax-0", + suggested_object_id="home_realfeel_temperature_shade_max_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-realfeeltemperatureshademin-0", + suggested_object_id="home_realfeel_temperature_shade_min_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-tree-0", + suggested_object_id="home_tree_pollen_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-windgustday-0", + suggested_object_id="home_wind_gust_day_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-windgustnight-0", + suggested_object_id="home_wind_gust_night_0d", + disabled_by=None, + ) + + await init_integration(hass, forecast=True) + + state = hass.states.get("sensor.home_apparent_temperature") + assert state + assert state.state == "22.8" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_apparent_temperature") + assert entry + assert entry.unique_id == "0123456-apparenttemperature" + + state = hass.states.get("sensor.home_cloud_cover") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" + + entry = registry.async_get("sensor.home_cloud_cover") + assert entry + assert entry.unique_id == "0123456-cloudcover" + + state = hass.states.get("sensor.home_dew_point") + assert state + assert state.state == "16.2" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_dew_point") + assert entry + assert entry.unique_id == "0123456-dewpoint" + + state = hass.states.get("sensor.home_realfeel_temperature_shade") + assert state + assert state.state == "21.1" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_realfeel_temperature_shade") + assert entry + assert entry.unique_id == "0123456-realfeeltemperatureshade" + + state = hass.states.get("sensor.home_wet_bulb_temperature") + assert state + assert state.state == "18.6" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_wet_bulb_temperature") + assert entry + assert entry.unique_id == "0123456-wetbulbtemperature" + + state = hass.states.get("sensor.home_wind_chill_temperature") + assert state + assert state.state == "22.8" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_wind_chill_temperature") + assert entry + assert entry.unique_id == "0123456-windchilltemperature" + + state = hass.states.get("sensor.home_wind_gust") + assert state + assert state.state == "20.3" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + + entry = registry.async_get("sensor.home_wind_gust") + assert entry + assert entry.unique_id == "0123456-windgust" + + state = hass.states.get("sensor.home_cloud_cover_day_0d") + assert state + assert state.state == "58" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" + + entry = registry.async_get("sensor.home_cloud_cover_day_0d") + assert entry + assert entry.unique_id == "0123456-cloudcoverday-0" + + state = hass.states.get("sensor.home_cloud_cover_night_0d") + assert state + assert state.state == "65" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" + + entry = registry.async_get("sensor.home_cloud_cover_night_0d") + assert entry + + state = hass.states.get("sensor.home_grass_pollen_0d") + assert state + assert state.state == "0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_CUBIC_METER + ) + assert state.attributes.get("level") == "Low" + assert state.attributes.get(ATTR_ICON) == "mdi:grass" + + entry = registry.async_get("sensor.home_grass_pollen_0d") + assert entry + assert entry.unique_id == "0123456-grass-0" + + state = hass.states.get("sensor.home_mold_pollen_0d") + assert state + assert state.state == "0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_CUBIC_METER + ) + assert state.attributes.get("level") == "Low" + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + + entry = registry.async_get("sensor.home_mold_pollen_0d") + assert entry + assert entry.unique_id == "0123456-mold-0" + + state = hass.states.get("sensor.home_ozone_0d") + assert state + assert state.state == "32" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get("level") == "Good" + assert state.attributes.get(ATTR_ICON) == "mdi:vector-triangle" + + entry = registry.async_get("sensor.home_ozone_0d") + assert entry + assert entry.unique_id == "0123456-ozone-0" + + state = hass.states.get("sensor.home_ragweed_pollen_0d") + assert state + assert state.state == "0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_CUBIC_METER + ) + assert state.attributes.get("level") == "Low" + assert state.attributes.get(ATTR_ICON) == "mdi:sprout" + + entry = registry.async_get("sensor.home_ragweed_pollen_0d") + assert entry + assert entry.unique_id == "0123456-ragweed-0" + + state = hass.states.get("sensor.home_realfeel_temperature_shade_max_0d") + assert state + assert state.state == "28.0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_realfeel_temperature_shade_max_0d") + assert entry + assert entry.unique_id == "0123456-realfeeltemperatureshademax-0" + + state = hass.states.get("sensor.home_realfeel_temperature_shade_min_0d") + assert state + assert state.state == "15.1" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + + entry = registry.async_get("sensor.home_realfeel_temperature_shade_min_0d") + assert entry + assert entry.unique_id == "0123456-realfeeltemperatureshademin-0" + + state = hass.states.get("sensor.home_tree_pollen_0d") + assert state + assert state.state == "0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_PARTS_PER_CUBIC_METER + ) + assert state.attributes.get("level") == "Low" + assert state.attributes.get(ATTR_ICON) == "mdi:tree-outline" + + entry = registry.async_get("sensor.home_tree_pollen_0d") + assert entry + assert entry.unique_id == "0123456-tree-0" + + state = hass.states.get("sensor.home_wind_gust_day_0d") + assert state + assert state.state == "29.6" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert state.attributes.get("direction") == "S" + assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + + entry = registry.async_get("sensor.home_wind_gust_day_0d") + assert entry + assert entry.unique_id == "0123456-windgustday-0" + + state = hass.states.get("sensor.home_wind_gust_night_0d") + assert state + assert state.state == "18.5" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert state.attributes.get("direction") == "WSW" + assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + + entry = registry.async_get("sensor.home_wind_gust_night_0d") + assert entry + assert entry.unique_id == "0123456-windgustnight-0" + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when service is offline.""" + await init_integration(hass) + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "3200" + + future = utcnow() + timedelta(minutes=60) + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + side_effect=ConnectionError(), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=120) + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=json.loads( + load_fixture("accuweather/current_conditions_data.json") + ), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "3200" + + +async def test_manual_update_entity(hass): + """Test manual update entity via service homeasasistant/update_entity.""" + await init_integration(hass, forecast=True) + + await async_setup_component(hass, "homeassistant", {}) + + current = json.loads(load_fixture("accuweather/current_conditions_data.json")) + forecast = json.loads(load_fixture("accuweather/forecast_data.json")) + + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ) as mock_current, patch( + "homeassistant.components.accuweather.AccuWeather.async_get_forecast", + return_value=forecast, + ) as mock_forecast: + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.home_cloud_ceiling"]}, + blocking=True, + ) + assert mock_current.call_count == 1 + assert mock_forecast.call_count == 1 diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py new file mode 100644 index 00000000000..692b2ae243f --- /dev/null +++ b/tests/components/accuweather/test_weather.py @@ -0,0 +1,155 @@ +"""Test weather of AccuWeather integration.""" +from datetime import timedelta +import json + +from homeassistant.components.accuweather.const import ATTRIBUTION +from homeassistant.components.weather import ( + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_OZONE, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, +) +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.async_mock import patch +from tests.common import async_fire_time_changed, load_fixture +from tests.components.accuweather import init_integration + + +async def test_weather_without_forecast(hass): + """Test states of the weather without forecast.""" + await init_integration(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("weather.home") + assert state + assert state.state == "sunny" + assert not state.attributes.get(ATTR_FORECAST) + assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 67 + assert not state.attributes.get(ATTR_WEATHER_OZONE) + assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1012.0 + assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 + assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 + assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + + entry = registry.async_get("weather.home") + assert entry + assert entry.unique_id == "0123456" + + +async def test_weather_with_forecast(hass): + """Test states of the weather with forecast.""" + await init_integration(hass, forecast=True) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("weather.home") + assert state + assert state.state == "sunny" + assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 67 + assert state.attributes.get(ATTR_WEATHER_OZONE) == 32 + assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1012.0 + assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 + assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 + assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + forecast = state.attributes.get(ATTR_FORECAST)[0] + assert forecast.get(ATTR_FORECAST_CONDITION) == "lightning-rainy" + assert forecast.get(ATTR_FORECAST_PRECIPITATION) == 4.8 + assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 58 + assert forecast.get(ATTR_FORECAST_TEMP) == 29.5 + assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 15.4 + assert forecast.get(ATTR_FORECAST_TIME) == "2020-07-26T05:00:00+00:00" + assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 166 + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 13.0 + + entry = registry.async_get("weather.home") + assert entry + assert entry.unique_id == "0123456" + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when service is offline.""" + await init_integration(hass) + + state = hass.states.get("weather.home") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "sunny" + + future = utcnow() + timedelta(minutes=60) + with patch( + "homeassistant.components.accuweather.AccuWeather._async_get_data", + side_effect=ConnectionError(), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("weather.home") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=120) + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=json.loads( + load_fixture("accuweather/current_conditions_data.json") + ), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("weather.home") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "sunny" + + +async def test_manual_update_entity(hass): + """Test manual update entity via service homeasasistant/update_entity.""" + await init_integration(hass, forecast=True) + + await async_setup_component(hass, "homeassistant", {}) + + current = json.loads(load_fixture("accuweather/current_conditions_data.json")) + forecast = json.loads(load_fixture("accuweather/forecast_data.json")) + + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ) as mock_current, patch( + "homeassistant.components.accuweather.AccuWeather.async_get_forecast", + return_value=forecast, + ) as mock_forecast: + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["weather.home"]}, + blocking=True, + ) + assert mock_current.call_count == 1 + assert mock_forecast.call_count == 1 + + +async def test_unsupported_condition_icon_data(hass): + """Test with unsupported condition icon data.""" + await init_integration(hass, forecast=True, unsupported_icon=True) + + state = hass.states.get("weather.home") + assert state.attributes.get(ATTR_FORECAST_CONDITION) is None diff --git a/tests/fixtures/accuweather/forecast_data.json b/tests/fixtures/accuweather/forecast_data.json new file mode 100644 index 00000000000..2de06dc66f4 --- /dev/null +++ b/tests/fixtures/accuweather/forecast_data.json @@ -0,0 +1,981 @@ +[ + { + "Date": "2020-07-26T07:00:00+02:00", + "EpochDate": 1595739600, + "HoursOfSun": 7.2, + "DegreeDaySummary": { + "Heating": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 4.0, + "Unit": "C", + "UnitType": 17 + } + }, + "Ozone": { + "Value": 32, + "Category": "Good", + "CategoryValue": 1 + }, + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 5, + "Category": "Moderate", + "CategoryValue": 2 + }, + "TemperatureMin": { + "Value": 15.4, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 29.5, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 15.1, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 29.8, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 15.1, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 28.0, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 17, + "IconPhraseDay": "Partly sunny w/ t-storms", + "HasPrecipitationDay": true, + "PrecipitationTypeDay": "Rain", + "PrecipitationIntensityDay": "Moderate", + "ShortPhraseDay": "A shower and t-storm around", + "LongPhraseDay": "Clouds and sunshine with a couple of showers and a thunderstorm around late this afternoon", + "PrecipitationProbabilityDay": 60, + "ThunderstormProbabilityDay": 40, + "RainProbabilityDay": 60, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 166, + "Localized": "SSE", + "English": "SSE" + } + }, + "WindGustDay": { + "Speed": { + "Value": 29.6, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 178, + "Localized": "S", + "English": "S" + } + }, + "TotalLiquidDay": { + "Value": 2.5, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 2.5, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 1.0, + "HoursOfRainDay": 1.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 58, + "IconNight": 41, + "IconPhraseNight": "Partly cloudy w/ t-storms", + "HasPrecipitationNight": true, + "PrecipitationTypeNight": "Rain", + "PrecipitationIntensityNight": "Moderate", + "ShortPhraseNight": "Partly cloudy", + "LongPhraseNight": "Partly cloudy", + "PrecipitationProbabilityNight": 57, + "ThunderstormProbabilityNight": 40, + "RainProbabilityNight": 57, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 7.4, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 289, + "Localized": "WNW", + "English": "WNW" + } + }, + "WindGustNight": { + "Speed": { + "Value": 18.5, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 256, + "Localized": "WSW", + "English": "WSW" + } + }, + "TotalLiquidNight": { + "Value": 2.3, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 2.3, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 1.0, + "HoursOfRainNight": 1.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 65 + }, + { + "Date": "2020-07-27T07:00:00+02:00", + "EpochDate": 1595826000, + "HoursOfSun": 7.4, + "DegreeDaySummary": { + "Heating": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 3.0, + "Unit": "C", + "UnitType": 17 + } + }, + "Ozone": { + "Value": 39, + "Category": "Good", + "CategoryValue": 1 + }, + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 7, + "Category": "High", + "CategoryValue": 3 + }, + "TemperatureMin": { + "Value": 15.9, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 26.2, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 15.8, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 28.9, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 15.8, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 25.0, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 4, + "IconPhraseDay": "Intermittent clouds", + "HasPrecipitationDay": false, + "ShortPhraseDay": "Clouds and sun", + "LongPhraseDay": "Clouds and sun", + "PrecipitationProbabilityDay": 25, + "ThunderstormProbabilityDay": 24, + "RainProbabilityDay": 25, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 9.3, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 297, + "Localized": "WNW", + "English": "WNW" + } + }, + "WindGustDay": { + "Speed": { + "Value": 14.8, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 317, + "Localized": "NW", + "English": "NW" + } + }, + "TotalLiquidDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 0.0, + "HoursOfRainDay": 0.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 52, + "IconNight": 36, + "IconPhraseNight": "Intermittent clouds", + "HasPrecipitationNight": false, + "ShortPhraseNight": "Partly cloudy", + "LongPhraseNight": "Partly cloudy", + "PrecipitationProbabilityNight": 6, + "ThunderstormProbabilityNight": 0, + "RainProbabilityNight": 6, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 7.4, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 162, + "Localized": "SSE", + "English": "SSE" + } + }, + "WindGustNight": { + "Speed": { + "Value": 14.8, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 175, + "Localized": "S", + "English": "S" + } + }, + "TotalLiquidNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 0.0, + "HoursOfRainNight": 0.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 63 + }, + { + "Date": "2020-07-28T07:00:00+02:00", + "EpochDate": 1595912400, + "HoursOfSun": 5.7, + "DegreeDaySummary": { + "Heating": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 6.0, + "Unit": "C", + "UnitType": 17 + } + }, + "Ozone": { + "Value": 29, + "Category": "Good", + "CategoryValue": 1 + }, + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 7, + "Category": "High", + "CategoryValue": 3 + }, + "TemperatureMin": { + "Value": 16.8, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 31.7, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 16.7, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 31.6, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 16.7, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 30.0, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 4, + "IconPhraseDay": "Intermittent clouds", + "HasPrecipitationDay": false, + "ShortPhraseDay": "Partly sunny and very warm", + "LongPhraseDay": "Very warm with a blend of sun and clouds", + "PrecipitationProbabilityDay": 10, + "ThunderstormProbabilityDay": 4, + "RainProbabilityDay": 10, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 16.7, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 198, + "Localized": "SSW", + "English": "SSW" + } + }, + "WindGustDay": { + "Speed": { + "Value": 24.1, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 198, + "Localized": "SSW", + "English": "SSW" + } + }, + "TotalLiquidDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 0.0, + "HoursOfRainDay": 0.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 65, + "IconNight": 36, + "IconPhraseNight": "Intermittent clouds", + "HasPrecipitationNight": false, + "ShortPhraseNight": "Partly cloudy", + "LongPhraseNight": "Partly cloudy", + "PrecipitationProbabilityNight": 25, + "ThunderstormProbabilityNight": 24, + "RainProbabilityNight": 25, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 9.3, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 265, + "Localized": "W", + "English": "W" + } + }, + "WindGustNight": { + "Speed": { + "Value": 22.2, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 271, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 0.0, + "HoursOfRainNight": 0.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 53 + }, + { + "Date": "2020-07-29T07:00:00+02:00", + "EpochDate": 1595998800, + "HoursOfSun": 9.4, + "DegreeDaySummary": { + "Heating": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + } + }, + "Ozone": { + "Value": 18, + "Category": "Good", + "CategoryValue": 1 + }, + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 6, + "Category": "High", + "CategoryValue": 3 + }, + "TemperatureMin": { + "Value": 11.7, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 24.0, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 10.1, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 26.5, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 10.1, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 22.5, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 3, + "IconPhraseDay": "Partly sunny", + "HasPrecipitationDay": false, + "ShortPhraseDay": "Cooler with partial sunshine", + "LongPhraseDay": "Cooler with partial sunshine", + "PrecipitationProbabilityDay": 9, + "ThunderstormProbabilityDay": 0, + "RainProbabilityDay": 9, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 13.0, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 293, + "Localized": "WNW", + "English": "WNW" + } + }, + "WindGustDay": { + "Speed": { + "Value": 24.1, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 271, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 0.0, + "HoursOfRainDay": 0.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 45, + "IconNight": 34, + "IconPhraseNight": "Mostly clear", + "HasPrecipitationNight": false, + "ShortPhraseNight": "Mainly clear", + "LongPhraseNight": "Mainly clear", + "PrecipitationProbabilityNight": 1, + "ThunderstormProbabilityNight": 0, + "RainProbabilityNight": 1, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 11.1, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 264, + "Localized": "W", + "English": "W" + } + }, + "WindGustNight": { + "Speed": { + "Value": 18.5, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 266, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 0.0, + "HoursOfRainNight": 0.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 27 + }, + { + "Date": "2020-07-30T07:00:00+02:00", + "EpochDate": 1596085200, + "HoursOfSun": 9.2, + "DegreeDaySummary": { + "Heating": { + "Value": 1.0, + "Unit": "C", + "UnitType": 17 + }, + "Cooling": { + "Value": 0.0, + "Unit": "C", + "UnitType": 17 + } + }, + "Ozone": { + "Value": 14, + "Category": "Good", + "CategoryValue": 1 + }, + "Grass": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Mold": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Ragweed": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "Tree": { + "Value": 0, + "Category": "Low", + "CategoryValue": 1 + }, + "UVIndex": { + "Value": 7, + "Category": "High", + "CategoryValue": 3 + }, + "TemperatureMin": { + "Value": 12.2, + "Unit": "C", + "UnitType": 17 + }, + "TemperatureMax": { + "Value": 21.4, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMin": { + "Value": 11.3, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureMax": { + "Value": 22.2, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMin": { + "Value": 11.3, + "Unit": "C", + "UnitType": 17 + }, + "RealFeelTemperatureShadeMax": { + "Value": 19.5, + "Unit": "C", + "UnitType": 17 + }, + "IconDay": 4, + "IconPhraseDay": "Intermittent clouds", + "HasPrecipitationDay": false, + "ShortPhraseDay": "Clouds and sun", + "LongPhraseDay": "Intervals of clouds and sunshine", + "PrecipitationProbabilityDay": 1, + "ThunderstormProbabilityDay": 0, + "RainProbabilityDay": 1, + "SnowProbabilityDay": 0, + "IceProbabilityDay": 0, + "WindDay": { + "Speed": { + "Value": 18.5, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 280, + "Localized": "W", + "English": "W" + } + }, + "WindGustDay": { + "Speed": { + "Value": 27.8, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 273, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowDay": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceDay": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationDay": 0.0, + "HoursOfRainDay": 0.0, + "HoursOfSnowDay": 0.0, + "HoursOfIceDay": 0.0, + "CloudCoverDay": 50, + "IconNight": 34, + "IconPhraseNight": "Mostly clear", + "HasPrecipitationNight": false, + "ShortPhraseNight": "Mostly clear", + "LongPhraseNight": "Mostly clear", + "PrecipitationProbabilityNight": 3, + "ThunderstormProbabilityNight": 0, + "RainProbabilityNight": 3, + "SnowProbabilityNight": 0, + "IceProbabilityNight": 0, + "WindNight": { + "Speed": { + "Value": 9.3, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 272, + "Localized": "W", + "English": "W" + } + }, + "WindGustNight": { + "Speed": { + "Value": 18.5, + "Unit": "km/h", + "UnitType": 7 + }, + "Direction": { + "Degrees": 274, + "Localized": "W", + "English": "W" + } + }, + "TotalLiquidNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "RainNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "SnowNight": { + "Value": 0.0, + "Unit": "cm", + "UnitType": 4 + }, + "IceNight": { + "Value": 0.0, + "Unit": "mm", + "UnitType": 3 + }, + "HoursOfPrecipitationNight": 0.0, + "HoursOfRainNight": 0.0, + "HoursOfSnowNight": 0.0, + "HoursOfIceNight": 0.0, + "CloudCoverNight": 13 + } +] \ No newline at end of file From f526deaa87c001391449ada6eae2e25fc6987671 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Sun, 9 Aug 2020 18:23:49 +0200 Subject: [PATCH 063/862] Bump version of aiopvpc to v2.0.2 (#38691) --- homeassistant/components/pvpc_hourly_pricing/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 023b22683f8..3f2dd00d832 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,7 +3,7 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==1.0.2"], + "requirements": ["aiopvpc==2.0.2"], "codeowners": ["@azogue"], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index d3ad3b1a28f..19c428ec822 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -210,7 +210,7 @@ aiopulse==0.4.0 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==1.0.2 +aiopvpc==2.0.2 # homeassistant.components.webostv aiopylgtv==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1714afa4805..64b7e0e08a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -120,7 +120,7 @@ aiopulse==0.4.0 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==1.0.2 +aiopvpc==2.0.2 # homeassistant.components.webostv aiopylgtv==0.3.3 From ef039d6a65c01c8184a528f40f883244e39f6743 Mon Sep 17 00:00:00 2001 From: Ian Duffy <1243435+imduffy15@users.noreply.github.com> Date: Sun, 9 Aug 2020 17:25:22 +0100 Subject: [PATCH 064/862] Fix Kodi play_media media type casing (#38665) * [KODI] Fix casing issue Alexa and the Services UI on HA feeds in a media type of "channel" for media type. The Kodi code looks for a "CHANNEL" instead, as a result the functionality fails. * Update homeassistant/components/kodi/media_player.py Co-authored-by: Chris Talkington * Update homeassistant/components/kodi/media_player.py Co-authored-by: Chris Talkington * Update homeassistant/components/kodi/media_player.py Co-authored-by: Chris Talkington * Update homeassistant/components/kodi/media_player.py Co-authored-by: Chris Talkington Co-authored-by: Chris Talkington --- homeassistant/components/kodi/media_player.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 81f8696a31a..e2dc15bab41 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -774,13 +774,15 @@ class KodiDevice(MediaPlayerEntity): @cmd async def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" - if media_type == "CHANNEL": + media_type_lower = media_type.lower() + + if media_type_lower == MEDIA_TYPE_CHANNEL: await self.server.Player.Open({"item": {"channelid": int(media_id)}}) - elif media_type == "PLAYLIST": + elif media_type_lower == MEDIA_TYPE_PLAYLIST: await self.server.Player.Open({"item": {"playlistid": int(media_id)}}) - elif media_type == "DIRECTORY": + elif media_type_lower == "directory": await self.server.Player.Open({"item": {"directory": str(media_id)}}) - elif media_type == "PLUGIN": + elif media_type_lower == "plugin": await self.server.Player.Open({"item": {"file": str(media_id)}}) else: await self.server.Player.Open({"item": {"file": str(media_id)}}) From 597d1e2799b398b7f2df7f5bb28104c5a7cf3476 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 9 Aug 2020 10:05:26 -0700 Subject: [PATCH 065/862] Add missing Short type to set_config_param (#38618) * Add missing Short type to set_config_param * Forgot to fix the for loop * Remove leftover return * Guard the for loop * Changed from if to else * Fixed list iteration for validating input * Adjusted per linter --- homeassistant/components/ozw/services.py | 107 ++++++++++------------- tests/components/ozw/test_services.py | 50 ++++++++--- 2 files changed, 84 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/ozw/services.py b/homeassistant/components/ozw/services.py index f950e68d2b4..b9281953ac9 100644 --- a/homeassistant/components/ozw/services.py +++ b/homeassistant/components/ozw/services.py @@ -68,73 +68,60 @@ class ZWaveServices: selection = service.data[const.ATTR_CONFIG_VALUE] payload = None - node = self._manager.get_instance(instance_id).get_node(node_id).values() + value = ( + self._manager.get_instance(instance_id) + .get_node(node_id) + .get_value(CommandClass.CONFIGURATION, param) + ) - for value in node: - if ( - value.command_class != CommandClass.CONFIGURATION - or value.index != param - ): - continue + if value.type == ValueType.BOOL: + payload = selection == "True" - if value.type == ValueType.BOOL: - payload = selection == "True" + if value.type == ValueType.LIST: + # accept either string from the list value OR the int value + for selected in value.value["List"]: + if selection not in (selected["Label"], selected["Value"]): + continue + payload = int(selected["Value"]) - if value.type == ValueType.LIST: - # accept either string from the list value OR the int value - if isinstance(selection, int): - if selection > value.max or selection < value.min: - _LOGGER.error( - "Value %s out of range for parameter %s (Min: %s Max: %s)", - selection, - param, - value.min, - value.max, - ) - return - payload = int(selection) - - # iterate list labels to get value - for selected in value.value["List"]: - if selected["Label"] != selection: - continue - payload = int(selected["Value"]) - - if payload is None: - _LOGGER.error( - "Invalid value %s for parameter %s", selection, param, - ) - return - - if value.type == ValueType.BUTTON: - # Unsupported at this time - _LOGGER.info("Button type not supported yet") + if payload is None: + _LOGGER.error( + "Invalid value %s for parameter %s", selection, param, + ) return - if value.type == ValueType.STRING: - payload = selection - - if value.type == ValueType.INT or value.type == ValueType.BYTE: - if selection > value.max or selection < value.min: - _LOGGER.error( - "Value %s out of range for parameter %s (Min: %s Max: %s)", - selection, - param, - value.min, - value.max, - ) - return - payload = int(selection) - - value.send_value(payload) # send the payload - _LOGGER.info( - "Setting configuration parameter %s on Node %s with value %s", - param, - node_id, - payload, - ) + if value.type == ValueType.BUTTON: + # Unsupported at this time + _LOGGER.info("Button type not supported yet") return + if value.type == ValueType.STRING: + payload = selection + + if ( + value.type == ValueType.INT + or value.type == ValueType.BYTE + or value.type == ValueType.SHORT + ): + if selection > value.max or selection < value.min: + _LOGGER.error( + "Value %s out of range for parameter %s (Min: %s Max: %s)", + selection, + param, + value.min, + value.max, + ) + return + payload = int(selection) + + value.send_value(payload) # send the payload + _LOGGER.info( + "Setting configuration parameter %s on Node %s with value %s", + param, + node_id, + payload, + ) + @callback def async_add_node(self, service): """Enter inclusion mode on the controller.""" diff --git a/tests/components/ozw/test_services.py b/tests/components/ozw/test_services.py index 9ea4e4f496b..1460e69b9c9 100644 --- a/tests/components/ozw/test_services.py +++ b/tests/components/ozw/test_services.py @@ -2,61 +2,61 @@ from .common import setup_ozw -async def test_services(hass, lock_data, sent_messages, lock_msg, caplog): +async def test_services(hass, light_data, sent_messages, light_msg, caplog): """Test services on lock.""" - await setup_ozw(hass, fixture=lock_data) + await setup_ozw(hass, fixture=light_data) # Test set_config_parameter list by label await hass.services.async_call( "ozw", "set_config_parameter", - {"node_id": 10, "parameter": 1, "value": "Disabled"}, + {"node_id": 39, "parameter": 1, "value": "Disable"}, blocking=True, ) assert len(sent_messages) == 1 msg = sent_messages[0] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 281475154706452} + assert msg["payload"] == {"Value": 0, "ValueIDKey": 281475641245716} # Test set_config_parameter list by index int await hass.services.async_call( "ozw", "set_config_parameter", - {"node_id": 10, "parameter": 1, "value": 0}, + {"node_id": 39, "parameter": 1, "value": 1}, blocking=True, ) assert len(sent_messages) == 2 msg = sent_messages[1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 281475154706452} + assert msg["payload"] == {"Value": 1, "ValueIDKey": 281475641245716} # Test set_config_parameter int await hass.services.async_call( "ozw", "set_config_parameter", - {"node_id": 10, "parameter": 6, "value": 0}, + {"node_id": 39, "parameter": 3, "value": 55}, blocking=True, ) assert len(sent_messages) == 3 msg = sent_messages[2] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 1688850038259731} + assert msg["payload"] == {"Value": 55, "ValueIDKey": 844425594667027} # Test set_config_parameter invalid list int await hass.services.async_call( "ozw", "set_config_parameter", - {"node_id": 10, "parameter": 1, "value": 12}, + {"node_id": 39, "parameter": 1, "value": 12}, blocking=True, ) assert len(sent_messages) == 3 - assert "Value 12 out of range for parameter 1" in caplog.text + assert "Invalid value 12 for parameter 1" in caplog.text # Test set_config_parameter invalid list string await hass.services.async_call( "ozw", "set_config_parameter", - {"node_id": 10, "parameter": 1, "value": "Blah"}, + {"node_id": 39, "parameter": 1, "value": "Blah"}, blocking=True, ) assert len(sent_messages) == 3 @@ -66,8 +66,32 @@ async def test_services(hass, lock_data, sent_messages, lock_msg, caplog): await hass.services.async_call( "ozw", "set_config_parameter", - {"node_id": 10, "parameter": 6, "value": 2147483657}, + {"node_id": 39, "parameter": 3, "value": 2147483657}, blocking=True, ) assert len(sent_messages) == 3 - assert "Value 12 out of range for parameter 1" in caplog.text + assert "Value 2147483657 out of range for parameter 3" in caplog.text + + # Test set_config_parameter short + await hass.services.async_call( + "ozw", + "set_config_parameter", + {"node_id": 39, "parameter": 81, "value": 3000}, + blocking=True, + ) + assert len(sent_messages) == 4 + msg = sent_messages[3] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 3000, "ValueIDKey": 22799473778098198} + + # Test set_config_parameter byte + await hass.services.async_call( + "ozw", + "set_config_parameter", + {"node_id": 39, "parameter": 16, "value": 20}, + blocking=True, + ) + assert len(sent_messages) == 5 + msg = sent_messages[4] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 20, "ValueIDKey": 4503600291905553} From 96d48c309fbbca5988b9d422c4ddf1387ed41f93 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sun, 9 Aug 2020 21:08:07 +0300 Subject: [PATCH 066/862] Make CoolMasterNet integration async (#38643) * make CoolMasteNet integration async * Apply suggestions from code review Co-authored-by: Chris Talkington * Minor post-review tweaks * Apply suggestions from code review * Update homeassistant/components/coolmaster/__init__.py Co-authored-by: Chris Talkington --- .../components/coolmaster/__init__.py | 54 ++++++- .../components/coolmaster/climate.py | 136 ++++++++++-------- .../components/coolmaster/config_flow.py | 10 +- homeassistant/components/coolmaster/const.py | 3 + .../components/coolmaster/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/coolmaster/test_config_flow.py | 15 +- 8 files changed, 144 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index c666c39cfb3..390b807f5cf 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -1,13 +1,41 @@ """The Coolmaster integration.""" +import logging + +from pycoolmasternet_async import CoolMasterNet + +from homeassistant.components.climate import SCAN_INTERVAL +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN + +_LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up Coolmaster components.""" + hass.data.setdefault(DOMAIN, {}) return True async def async_setup_entry(hass, entry): """Set up Coolmaster from a config entry.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + coolmaster = CoolMasterNet(host, port) + try: + info = await coolmaster.info() + if not info: + raise ConfigEntryNotReady + except (OSError, ConnectionRefusedError, TimeoutError) as error: + raise ConfigEntryNotReady() from error + coordinator = CoolmasterDataUpdateCoordinator(hass, coolmaster) + await coordinator.async_refresh() + hass.data[DOMAIN][entry.entry_id] = { + DATA_INFO: info, + DATA_COORDINATOR: coordinator, + } hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "climate") ) @@ -17,4 +45,28 @@ async def async_setup_entry(hass, entry): async def async_unload_entry(hass, entry): """Unload a Coolmaster config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, "climate") + unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "climate") + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class CoolmasterDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Coolmaster data.""" + + def __init__(self, hass, coolmaster): + """Initialize global Coolmaster data updater.""" + self._coolmaster = coolmaster + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self): + """Fetch data from Coolmaster.""" + try: + return await self._coolmaster.status() + except (OSError, ConnectionRefusedError, TimeoutError) as error: + raise UpdateFailed from error diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 6e68e858a6d..8666307d65c 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -2,8 +2,6 @@ import logging -from pycoolmasternet import CoolMasterNet - from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_COOL, @@ -15,15 +13,9 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_HOST, - CONF_PORT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from .const import CONF_SUPPORTED_MODES, DOMAIN +from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE @@ -42,58 +34,63 @@ FAN_MODES = ["low", "med", "high", "auto"] _LOGGER = logging.getLogger(__name__) -def _build_entity(device, supported_modes): - _LOGGER.debug("Found device %s", device.uid) - return CoolmasterClimate(device, supported_modes) +def _build_entity(coordinator, unit_id, unit, supported_modes, info): + _LOGGER.debug("Found device %s", unit_id) + return CoolmasterClimate(coordinator, unit_id, unit, supported_modes, info) async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the CoolMasterNet climate platform.""" supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) - host = config_entry.data[CONF_HOST] - port = config_entry.data[CONF_PORT] - cool = CoolMasterNet(host, port=port) - devices = await hass.async_add_executor_job(cool.devices) + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] - all_devices = [_build_entity(device, supported_modes) for device in devices] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - async_add_devices(all_devices, True) + all_devices = [ + _build_entity(coordinator, unit_id, unit, supported_modes, info) + for (unit_id, unit) in coordinator.data.items() + ] + + async_add_devices(all_devices) class CoolmasterClimate(ClimateEntity): """Representation of a coolmaster climate device.""" - def __init__(self, device, supported_modes): + def __init__(self, coordinator, unit_id, unit, supported_modes, info): """Initialize the climate device.""" - self._device = device - self._uid = device.uid + self._coordinator = coordinator + self._unit_id = unit_id + self._unit = unit self._hvac_modes = supported_modes - self._hvac_mode = None - self._target_temperature = None - self._current_temperature = None - self._current_fan_mode = None - self._current_operation = None - self._on = None - self._unit = None + self._info = info - def update(self): - """Pull state from CoolMasterNet.""" - status = self._device.status - self._target_temperature = status["thermostat"] - self._current_temperature = status["temperature"] - self._current_fan_mode = status["fan_speed"] - self._on = status["is_on"] + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False - device_mode = status["mode"] - if self._on: - self._hvac_mode = CM_TO_HA_STATE[device_mode] - else: - self._hvac_mode = HVAC_MODE_OFF + @property + def available(self): + """Return if entity is available.""" + return self._coordinator.last_update_success - if status["unit"] == "celsius": - self._unit = TEMP_CELSIUS - else: - self._unit = TEMP_FAHRENHEIT + def _refresh_from_coordinator(self): + self._unit = self._coordinator.data[self._unit_id] + self.async_write_ha_state() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self._refresh_from_coordinator) + ) + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self._coordinator.async_request_refresh() @property def device_info(self): @@ -103,12 +100,13 @@ class CoolmasterClimate(ClimateEntity): "name": self.name, "manufacturer": "CoolAutomation", "model": "CoolMasterNet", + "sw_version": self._info["version"], } @property def unique_id(self): """Return unique ID for this device.""" - return self._uid + return self._unit_id @property def supported_features(self): @@ -123,22 +121,30 @@ class CoolmasterClimate(ClimateEntity): @property def temperature_unit(self): """Return the unit of measurement.""" - return self._unit + if self._unit.temperature_unit == "celsius": + return TEMP_CELSIUS + + return TEMP_FAHRENHEIT @property def current_temperature(self): """Return the current temperature.""" - return self._current_temperature + return self._unit.temperature @property def target_temperature(self): """Return the temperature we are trying to reach.""" - return self._target_temperature + return self._unit.thermostat @property def hvac_mode(self): """Return hvac target hvac state.""" - return self._hvac_mode + mode = self._unit.mode + is_on = self._unit.is_on + if not is_on: + return HVAC_MODE_OFF + + return CM_TO_HA_STATE[mode] @property def hvac_modes(self): @@ -148,41 +154,45 @@ class CoolmasterClimate(ClimateEntity): @property def fan_mode(self): """Return the fan setting.""" - return self._current_fan_mode + return self._unit.fan_speed @property def fan_modes(self): """Return the list of available fan modes.""" return FAN_MODES - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is not None: _LOGGER.debug("Setting temp of %s to %s", self.unique_id, str(temp)) - self._device.set_thermostat(str(temp)) + self._unit = await self._unit.set_thermostat(temp) + self.async_write_ha_state() - def set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new fan mode.""" _LOGGER.debug("Setting fan mode of %s to %s", self.unique_id, fan_mode) - self._device.set_fan_speed(fan_mode) + self._unit = await self._unit.set_fan_speed(fan_mode) + self.async_write_ha_state() - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode): """Set new operation mode.""" _LOGGER.debug("Setting operation mode of %s to %s", self.unique_id, hvac_mode) if hvac_mode == HVAC_MODE_OFF: - self.turn_off() + await self.async_turn_off() else: - self._device.set_mode(HA_STATE_TO_CM[hvac_mode]) - self.turn_on() + self._unit = await self._unit.set_mode(HA_STATE_TO_CM[hvac_mode]) + await self.async_turn_on() - def turn_on(self): + async def async_turn_on(self): """Turn on.""" _LOGGER.debug("Turning %s on", self.unique_id) - self._device.turn_on() + self._unit = await self._unit.turn_on() + self.async_write_ha_state() - def turn_off(self): + async def async_turn_off(self): """Turn off.""" _LOGGER.debug("Turning %s off", self.unique_id) - self._device.turn_off() + self._unit = await self._unit.turn_off() + self.async_write_ha_state() diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index c267b283118..c674146fd15 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -1,6 +1,6 @@ """Config flow to configure Coolmaster.""" -from pycoolmasternet import CoolMasterNet +from pycoolmasternet_async import CoolMasterNet import voluptuous as vol from homeassistant import config_entries, core @@ -15,9 +15,9 @@ DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) async def _validate_connection(hass: core.HomeAssistant, host): - cool = CoolMasterNet(host, port=DEFAULT_PORT) - devices = await hass.async_add_executor_job(cool.devices) - return bool(devices) + cool = CoolMasterNet(host, DEFAULT_PORT) + units = await cool.status() + return bool(units) class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -53,7 +53,7 @@ class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): result = await _validate_connection(self.hass, host) if not result: errors["base"] = "no_units" - except (ConnectionRefusedError, TimeoutError): + except (OSError, ConnectionRefusedError, TimeoutError): errors["base"] = "connection_error" if errors: diff --git a/homeassistant/components/coolmaster/const.py b/homeassistant/components/coolmaster/const.py index d4cfea73820..c07cbe392ef 100644 --- a/homeassistant/components/coolmaster/const.py +++ b/homeassistant/components/coolmaster/const.py @@ -9,6 +9,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, ) +DATA_INFO = "info" +DATA_COORDINATOR = "coordinator" + DOMAIN = "coolmaster" DEFAULT_PORT = 10102 diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index bc0ebd17d40..513dc495da3 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -3,6 +3,6 @@ "name": "CoolMasterNet", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", - "requirements": ["pycoolmasternet==0.0.4"], + "requirements": ["pycoolmasternet-async==0.1.0"], "codeowners": ["@OnFreund"] } diff --git a/requirements_all.txt b/requirements_all.txt index 19c428ec822..6f8b98f14d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1268,7 +1268,7 @@ pycocotools==2.0.1 pycomfoconnect==0.3 # homeassistant.components.coolmaster -pycoolmasternet==0.0.4 +pycoolmasternet-async==0.1.0 # homeassistant.components.avri pycountry==19.8.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64b7e0e08a6..71506e12d0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ pybotvac==0.0.17 pychromecast==7.2.0 # homeassistant.components.coolmaster -pycoolmasternet==0.0.4 +pycoolmasternet-async==0.1.0 # homeassistant.components.avri pycountry==19.8.18 diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index 49058fc183e..88985f7f88a 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Coolmaster config flow.""" -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.coolmaster.const import AVAILABLE_MODES, DOMAIN from tests.async_mock import patch @@ -14,7 +14,6 @@ def _flow_data(): 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} ) @@ -22,8 +21,8 @@ async def test_form(hass): assert result["errors"] is None with patch( - "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", - return_value=[1], + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", + return_value={"test_id": "test_unit"}, ), patch( "homeassistant.components.coolmaster.async_setup", return_value=True ) as mock_setup, patch( @@ -52,7 +51,7 @@ async def test_form_timeout(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", side_effect=TimeoutError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -70,7 +69,7 @@ async def test_form_connection_refused(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", side_effect=ConnectionRefusedError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -88,8 +87,8 @@ async def test_form_no_units(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", - return_value=[], + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.status", + return_value={}, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], _flow_data() From 761067559d42db1658fe6a7975261fb0233b550d Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Sun, 9 Aug 2020 15:15:56 -0300 Subject: [PATCH 067/862] Add Nightscout integration (#38615) * Implement NightScout sensor integration * Add tests for NightScout integration * Fix Nightscout captalization * Change quality scale for Nightscout * Trigger actions * Add missing tests * Fix stale comments * Fix Nightscout manufacturer * Add entry type service * Change host to URL on nightscout config flow * Add ConfigEntryNotReady exception to nighscout init * Remote platform_schema from nightscout sensor * Update homeassistant/components/nightscout/config_flow.py Co-authored-by: Chris Talkington Co-authored-by: Chris Talkington --- CODEOWNERS | 1 + .../components/nightscout/__init__.py | 73 ++++++++++ .../components/nightscout/config_flow.py | 58 ++++++++ homeassistant/components/nightscout/const.py | 9 ++ .../components/nightscout/manifest.json | 13 ++ homeassistant/components/nightscout/sensor.py | 126 ++++++++++++++++++ .../components/nightscout/strings.json | 16 +++ .../nightscout/translations/ca.json | 15 +++ .../nightscout/translations/de.json | 15 +++ .../nightscout/translations/en.json | 15 +++ .../nightscout/translations/es.json | 15 +++ .../nightscout/translations/fr.json | 15 +++ .../nightscout/translations/it.json | 15 +++ .../nightscout/translations/ko.json | 15 +++ .../nightscout/translations/lb.json | 14 ++ .../nightscout/translations/pl.json | 15 +++ .../nightscout/translations/pt.json | 15 +++ .../nightscout/translations/ru.json | 15 +++ .../nightscout/translations/sl.json | 15 +++ .../nightscout/translations/zh-Hant.json | 15 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nightscout/__init__.py | 74 ++++++++++ .../components/nightscout/test_config_flow.py | 85 ++++++++++++ tests/components/nightscout/test_init.py | 43 ++++++ tests/components/nightscout/test_sensor.py | 60 +++++++++ 27 files changed, 759 insertions(+) create mode 100644 homeassistant/components/nightscout/__init__.py create mode 100644 homeassistant/components/nightscout/config_flow.py create mode 100644 homeassistant/components/nightscout/const.py create mode 100644 homeassistant/components/nightscout/manifest.json create mode 100644 homeassistant/components/nightscout/sensor.py create mode 100644 homeassistant/components/nightscout/strings.json create mode 100644 homeassistant/components/nightscout/translations/ca.json create mode 100644 homeassistant/components/nightscout/translations/de.json create mode 100644 homeassistant/components/nightscout/translations/en.json create mode 100644 homeassistant/components/nightscout/translations/es.json create mode 100644 homeassistant/components/nightscout/translations/fr.json create mode 100644 homeassistant/components/nightscout/translations/it.json create mode 100644 homeassistant/components/nightscout/translations/ko.json create mode 100644 homeassistant/components/nightscout/translations/lb.json create mode 100644 homeassistant/components/nightscout/translations/pl.json create mode 100644 homeassistant/components/nightscout/translations/pt.json create mode 100644 homeassistant/components/nightscout/translations/ru.json create mode 100644 homeassistant/components/nightscout/translations/sl.json create mode 100644 homeassistant/components/nightscout/translations/zh-Hant.json create mode 100644 tests/components/nightscout/__init__.py create mode 100644 tests/components/nightscout/test_config_flow.py create mode 100644 tests/components/nightscout/test_init.py create mode 100644 tests/components/nightscout/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 0081057f086..3a8bfaa4842 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -272,6 +272,7 @@ homeassistant/components/netdata/* @fabaff homeassistant/components/nexia/* @ryannazaretian @bdraco homeassistant/components/nextbus/* @vividboarder homeassistant/components/nextcloud/* @meichthys +homeassistant/components/nightscout/* @marciogranzotto homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py new file mode 100644 index 00000000000..91cf056689c --- /dev/null +++ b/homeassistant/components/nightscout/__init__.py @@ -0,0 +1,73 @@ +"""The Nightscout integration.""" +import asyncio +from asyncio import TimeoutError as AsyncIOTimeoutError +import logging + +from aiohttp import ClientError +from py_nightscout import Api as NightscoutAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import SLOW_UPDATE_WARNING + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] +_API_TIMEOUT = SLOW_UPDATE_WARNING - 1 + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Nightscout component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Nightscout from a config entry.""" + server_url = entry.data[CONF_URL] + + api = NightscoutAPI(server_url) + try: + status = await api.get_server_status() + except (ClientError, AsyncIOTimeoutError, OSError) as error: + raise ConfigEntryNotReady from error + + hass.data[DOMAIN][entry.entry_id] = api + + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, server_url)}, + manufacturer="Nightscout Foundation", + name=status.name, + sw_version=status.version, + entry_type="service", + ) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py new file mode 100644 index 00000000000..c54293798ea --- /dev/null +++ b/homeassistant/components/nightscout/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow for Nightscout integration.""" +from asyncio import TimeoutError as AsyncIOTimeoutError +import logging + +from aiohttp import ClientError +from py_nightscout import Api as NightscoutAPI +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_URL + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL): str}) + + +async def _validate_input(data): + """Validate the user input allows us to connect.""" + + try: + api = NightscoutAPI(data[CONF_URL]) + status = await api.get_server_status() + except (ClientError, AsyncIOTimeoutError, OSError): + raise CannotConnect + + # Return info to be stored in the config entry. + return {"title": status.name} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nightscout.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await _validate_input(user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/nightscout/const.py b/homeassistant/components/nightscout/const.py new file mode 100644 index 00000000000..f07f37b7b0c --- /dev/null +++ b/homeassistant/components/nightscout/const.py @@ -0,0 +1,9 @@ +"""Constants for the Nightscout integration.""" + +DOMAIN = "nightscout" + +ATTR_DEVICE = "device" +ATTR_DATE = "date" +ATTR_SVG = "svg" +ATTR_DELTA = "delta" +ATTR_DIRECTION = "direction" diff --git a/homeassistant/components/nightscout/manifest.json b/homeassistant/components/nightscout/manifest.json new file mode 100644 index 00000000000..b3e9b3a0d55 --- /dev/null +++ b/homeassistant/components/nightscout/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "nightscout", + "name": "Nightscout", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/nightscout", + "requirements": [ + "py-nightscout==1.2.1" + ], + "codeowners": [ + "@marciogranzotto" + ], + "quality_scale": "platinum" +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py new file mode 100644 index 00000000000..fce967b60d5 --- /dev/null +++ b/homeassistant/components/nightscout/sensor.py @@ -0,0 +1,126 @@ +"""Support for Nightscout sensors.""" +from asyncio import TimeoutError as AsyncIOTimeoutError +from datetime import timedelta +import hashlib +import logging +from typing import Callable, List + +from aiohttp import ClientError +from py_nightscout import Api as NightscoutAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from .const import ATTR_DATE, ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, ATTR_SVG, DOMAIN + +SCAN_INTERVAL = timedelta(minutes=1) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Blood Glucose" + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up the Glucose Sensor.""" + api = hass.data[DOMAIN][entry.entry_id] + async_add_entities([NightscoutSensor(api, "Blood Sugar")], True) + + +class NightscoutSensor(Entity): + """Implementation of a Nightscout sensor.""" + + def __init__(self, api: NightscoutAPI, name): + """Initialize the Nightscout sensor.""" + self.api = api + self._unique_id = hashlib.sha256(api.server_url.encode("utf-8")).hexdigest() + self._name = name + self._state = None + self._attributes = None + self._unit_of_measurement = "mg/dL" + self._icon = "mdi:cloud-question" + self._available = False + + @property + def unique_id(self): + """Return the unique ID of the sensor.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def available(self): + """Return if the sensor data are available.""" + return self._available + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + + @property + def should_poll(self): + """Return the polling state.""" + return True + + async def async_update(self): + """Fetch the latest data from Nightscout REST API and update the state.""" + try: + values = await self.api.get_sgvs() + except (ClientError, AsyncIOTimeoutError, OSError) as error: + _LOGGER.error("Error fetching data. Failed with %s", error) + self._available = False + return + + self._available = True + self._attributes = {} + self._state = None + if values: + value = values[0] + self._attributes = { + ATTR_DEVICE: value.device, + ATTR_DATE: value.date, + ATTR_SVG: value.sgv, + ATTR_DELTA: value.delta, + ATTR_DIRECTION: value.direction, + } + self._state = value.sgv + self._icon = self._parse_icon() + else: + self._available = False + _LOGGER.warning("Empty reply found when expecting JSON data") + + def _parse_icon(self) -> str: + """Update the icon based on the direction attribute.""" + switcher = { + "Flat": "mdi:arrow-right", + "SingleDown": "mdi:arrow-down", + "FortyFiveDown": "mdi:arrow-bottom-right", + "DoubleDown": "mdi:chevron-triple-down", + "SingleUp": "mdi:arrow-up", + "FortyFiveUp": "mdi:arrow-top-right", + "DoubleUp": "mdi:chevron-triple-up", + } + return switcher.get(self._attributes[ATTR_DIRECTION], "mdi:cloud-question") + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json new file mode 100644 index 00000000000..08b2bf09361 --- /dev/null +++ b/homeassistant/components/nightscout/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "url": "URL" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ca.json b/homeassistant/components/nightscout/translations/ca.json new file mode 100644 index 00000000000..d9b06cbe61e --- /dev/null +++ b/homeassistant/components/nightscout/translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json new file mode 100644 index 00000000000..9c76dd92f9a --- /dev/null +++ b/homeassistant/components/nightscout/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung nicht m\u00f6glich", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json new file mode 100644 index 00000000000..c67479819ea --- /dev/null +++ b/homeassistant/components/nightscout/translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/es.json b/homeassistant/components/nightscout/translations/es.json new file mode 100644 index 00000000000..05545cdbc48 --- /dev/null +++ b/homeassistant/components/nightscout/translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json new file mode 100644 index 00000000000..037992b12a3 --- /dev/null +++ b/homeassistant/components/nightscout/translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Echec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/it.json b/homeassistant/components/nightscout/translations/it.json new file mode 100644 index 00000000000..2f0790586f3 --- /dev/null +++ b/homeassistant/components/nightscout/translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json new file mode 100644 index 00000000000..66c2c8822b2 --- /dev/null +++ b/homeassistant/components/nightscout/translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "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": { + "url": "URL \uc8fc\uc18c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/lb.json b/homeassistant/components/nightscout/translations/lb.json new file mode 100644 index 00000000000..ca7db64f416 --- /dev/null +++ b/homeassistant/components/nightscout/translations/lb.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/pl.json b/homeassistant/components/nightscout/translations/pl.json new file mode 100644 index 00000000000..bf0d9900695 --- /dev/null +++ b/homeassistant/components/nightscout/translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/pt.json b/homeassistant/components/nightscout/translations/pt.json new file mode 100644 index 00000000000..218e7941b42 --- /dev/null +++ b/homeassistant/components/nightscout/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ru.json b/homeassistant/components/nightscout/translations/ru.json new file mode 100644 index 00000000000..a7bd268d56a --- /dev/null +++ b/homeassistant/components/nightscout/translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/sl.json b/homeassistant/components/nightscout/translations/sl.json new file mode 100644 index 00000000000..33b65a99f8a --- /dev/null +++ b/homeassistant/components/nightscout/translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Povezava ni uspela", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json new file mode 100644 index 00000000000..cf83adfc35a --- /dev/null +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "url": "\u7db2\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3b4216377e5..1bf776f0849 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -114,6 +114,7 @@ FLOWS = [ "nest", "netatmo", "nexia", + "nightscout", "notion", "nuheat", "nut", diff --git a/requirements_all.txt b/requirements_all.txt index 6f8b98f14d9..8549dddf313 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1161,6 +1161,9 @@ py-cpuinfo==5.0.0 # homeassistant.components.melissa py-melissa-climate==2.0.0 +# homeassistant.components.nightscout +py-nightscout==1.2.1 + # homeassistant.components.schluter py-schluter==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71506e12d0f..6305c3ae1e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -548,6 +548,9 @@ py-canary==0.5.0 # homeassistant.components.melissa py-melissa-climate==2.0.0 +# homeassistant.components.nightscout +py-nightscout==1.2.1 + # homeassistant.components.seventeentrack py17track==2.2.2 diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py new file mode 100644 index 00000000000..de1bb8a2b7c --- /dev/null +++ b/tests/components/nightscout/__init__.py @@ -0,0 +1,74 @@ +"""Tests for the Nightscout integration.""" +import json + +from aiohttp import ClientConnectionError +from py_nightscout.models import SGV, ServerStatus + +from homeassistant.components.nightscout.const import DOMAIN +from homeassistant.const import CONF_URL + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +GLUCOSE_READINGS = [ + SGV.new_from_json_dict( + json.loads( + '{"_id":"5f2b01f5c3d0ac7c4090e223","device":"xDrip-LimiTTer","date":1596654066533,"dateString":"2020-08-05T19:01:06.533Z","sgv":169,"delta":-5.257,"direction":"FortyFiveDown","type":"sgv","filtered":182823.5157,"unfiltered":182823.5157,"rssi":100,"noise":1,"sysTime":"2020-08-05T19:01:06.533Z","utcOffset":-180}' + ) + ) +] +SERVER_STATUS = ServerStatus.new_from_json_dict( + json.loads( + '{"status":"ok","name":"nightscout","version":"13.0.1","serverTime":"2020-08-05T18:14:02.032Z","serverTimeEpoch":1596651242032,"apiEnabled":true,"careportalEnabled":true,"boluscalcEnabled":true,"settings":{},"extendedSettings":{},"authorized":null}' + ) +) + + +async def init_integration(hass) -> MockConfigEntry: + """Set up the Nightscout integration in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},) + with patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", + return_value=GLUCOSE_READINGS, + ), patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + return_value=SERVER_STATUS, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +async def init_integration_unavailable(hass) -> MockConfigEntry: + """Set up the Nightscout integration in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},) + with patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", + side_effect=ClientConnectionError(), + ), patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + return_value=SERVER_STATUS, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +async def init_integration_empty_response(hass) -> MockConfigEntry: + """Set up the Nightscout integration in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},) + with patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", return_value=[] + ), patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + return_value=SERVER_STATUS, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py new file mode 100644 index 00000000000..9db86759658 --- /dev/null +++ b/tests/components/nightscout/test_config_flow.py @@ -0,0 +1,85 @@ +"""Test the Nightscout config flow.""" +from aiohttp import ClientConnectionError + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.nightscout.const import DOMAIN +from homeassistant.const import CONF_URL + +from tests.async_mock import patch +from tests.components.nightscout import GLUCOSE_READINGS, SERVER_STATUS + +CONFIG = {CONF_URL: "https://some.url:1234"} + + +async def test_form(hass): + """Test we get the user initiated 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"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", + return_value=GLUCOSE_READINGS, + ), patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + return_value=SERVER_STATUS, + ), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == SERVER_STATUS.name # pylint: disable=maybe-no-member + assert result2["data"] == CONFIG + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + side_effect=ClientConnectionError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_URL: "https://some.url:1234"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_user_form_unexpected_exception(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + side_effect=Exception(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_URL: "https://some.url:1234"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +def _patch_async_setup(): + return patch("homeassistant.components.nightscout.async_setup", return_value=True) + + +def _patch_async_setup_entry(): + return patch( + "homeassistant.components.nightscout.async_setup_entry", return_value=True, + ) diff --git a/tests/components/nightscout/test_init.py b/tests/components/nightscout/test_init.py new file mode 100644 index 00000000000..94953b7e5b2 --- /dev/null +++ b/tests/components/nightscout/test_init.py @@ -0,0 +1,43 @@ +"""Test the Nightscout config flow.""" +from aiohttp import ClientError + +from homeassistant.components.nightscout.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import CONF_URL + +from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.components.nightscout import init_integration + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_NOT_LOADED + assert not hass.data.get(DOMAIN) + + +async def test_async_setup_raises_entry_not_ready(hass): + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_URL: "https://some.url:1234"}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + side_effect=ClientError(), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/components/nightscout/test_sensor.py b/tests/components/nightscout/test_sensor.py new file mode 100644 index 00000000000..c2fcfe543e7 --- /dev/null +++ b/tests/components/nightscout/test_sensor.py @@ -0,0 +1,60 @@ +"""The sensor tests for the Nightscout platform.""" + +from homeassistant.components.nightscout.const import ( + ATTR_DATE, + ATTR_DELTA, + ATTR_DEVICE, + ATTR_DIRECTION, + ATTR_SVG, +) +from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE + +from tests.components.nightscout import ( + GLUCOSE_READINGS, + init_integration, + init_integration_empty_response, + init_integration_unavailable, +) + + +async def test_sensor_state(hass): + """Test sensor state data.""" + await init_integration(hass) + + test_glucose_sensor = hass.states.get("sensor.blood_sugar") + assert test_glucose_sensor.state == str( + GLUCOSE_READINGS[0].sgv # pylint: disable=maybe-no-member + ) + + +async def test_sensor_error(hass): + """Test sensor state data.""" + await init_integration_unavailable(hass) + + test_glucose_sensor = hass.states.get("sensor.blood_sugar") + assert test_glucose_sensor.state == STATE_UNAVAILABLE + + +async def test_sensor_empty_response(hass): + """Test sensor state data.""" + await init_integration_empty_response(hass) + + test_glucose_sensor = hass.states.get("sensor.blood_sugar") + assert test_glucose_sensor.state == STATE_UNAVAILABLE + + +async def test_sensor_attributes(hass): + """Test sensor attributes.""" + await init_integration(hass) + + test_glucose_sensor = hass.states.get("sensor.blood_sugar") + reading = GLUCOSE_READINGS[0] + assert reading is not None + + attr = test_glucose_sensor.attributes + assert attr[ATTR_DATE] == reading.date # pylint: disable=maybe-no-member + assert attr[ATTR_DELTA] == reading.delta # pylint: disable=maybe-no-member + assert attr[ATTR_DEVICE] == reading.device # pylint: disable=maybe-no-member + assert attr[ATTR_DIRECTION] == reading.direction # pylint: disable=maybe-no-member + assert attr[ATTR_SVG] == reading.sgv # pylint: disable=maybe-no-member + assert attr[ATTR_ICON] == "mdi:arrow-bottom-right" From b258e757c2d5eb2dad809fba50b95f7b6836afa0 Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Sun, 9 Aug 2020 20:18:02 +0200 Subject: [PATCH 068/862] Add DataUpdateCoordinator to met integration (#38405) * Add DataUpdateCoordinator to met integration * isort * redundant fetch_data * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington * fix black, isort, flake8, hassfest, mypy * remove unused async_setup method * replace fetch_data by coordinator request_refresh * remove redundant async_update * track_home * Apply suggestions from code review * Apply suggestions from code review * Update homeassistant/components/met/__init__.py * Apply suggestions from code review * Update homeassistant/components/met/__init__.py * Apply suggestions from code review * Update homeassistant/components/met/__init__.py * Apply suggestions from code review * Update test_config_flow.py * Apply suggestions from code review * Apply suggestions from code review * Update __init__.py * Create test_init.py * Update homeassistant/components/met/__init__.py * Update __init__.py * Update __init__.py * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington --- homeassistant/components/met/__init__.py | 135 +++++++++++++++++++- homeassistant/components/met/config_flow.py | 8 ++ homeassistant/components/met/weather.py | 118 ++++------------- tests/components/met/__init__.py | 25 ++++ tests/components/met/test_config_flow.py | 18 +++ tests/components/met/test_init.py | 19 +++ 6 files changed, 227 insertions(+), 96 deletions(-) create mode 100644 tests/components/met/test_init.py diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 7ef3c5f8796..32b3e939c43 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,24 +1,153 @@ """The met component.""" -from homeassistant.core import Config, HomeAssistant +from datetime import timedelta +import logging +from random import randrange -from .config_flow import MetFlowHandler # noqa: F401 -from .const import DOMAIN # noqa: F401 +import metno + +from homeassistant.const import ( + CONF_ELEVATION, + CONF_LATITUDE, + CONF_LONGITUDE, + EVENT_CORE_CONFIG_UPDATE, + LENGTH_FEET, + LENGTH_METERS, +) +from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.distance import convert as convert_distance +import homeassistant.util.dt as dt_util + +from .const import CONF_TRACK_HOME, DOMAIN + +URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/" + + +_LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up configured Met.""" + hass.data.setdefault(DOMAIN, {}) return True async def async_setup_entry(hass, config_entry): """Set up Met as config entry.""" + coordinator = MetDataUpdateCoordinator(hass, config_entry) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + if config_entry.data.get(CONF_TRACK_HOME, False): + coordinator.track_home() + + hass.data[DOMAIN][config_entry.entry_id] = coordinator + hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, "weather") ) + return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" await hass.config_entries.async_forward_entry_unload(config_entry, "weather") + hass.data[DOMAIN][config_entry.entry_id].untrack_home() + hass.data[DOMAIN].pop(config_entry.entry_id) + return True + + +class MetDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Met data.""" + + def __init__(self, hass, config_entry): + """Initialize global Met data updater.""" + self._unsub_track_home = None + self.weather = MetWeatherData( + hass, config_entry.data, hass.config.units.is_metric + ) + self.weather.init_data() + + update_interval = timedelta(minutes=randrange(55, 65)) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self): + """Fetch data from Met.""" + try: + return await self.weather.fetch_data() + except Exception as err: + raise UpdateFailed(f"Update failed: {err}") + + def track_home(self): + """Start tracking changes to HA home setting.""" + if self._unsub_track_home: + return + + async def _async_update_weather_data(_event=None): + """Update weather data.""" + self.weather.init_data() + await self.async_refresh() + + self._unsub_track_home = self.hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data + ) + + def untrack_home(self): + """Stop tracking changes to HA home setting.""" + if self._unsub_track_home: + self._unsub_track_home() + self._unsub_track_home = None + + +class MetWeatherData: + """Keep data for Met.no weather entities.""" + + def __init__(self, hass, config, is_metric): + """Initialise the weather entity data.""" + self.hass = hass + self._config = config + self._is_metric = is_metric + self._weather_data = None + self.current_weather_data = {} + self.forecast_data = None + + def init_data(self): + """Weather data inialization - get the coordinates.""" + if self._config.get(CONF_TRACK_HOME, False): + latitude = self.hass.config.latitude + longitude = self.hass.config.longitude + elevation = self.hass.config.elevation + else: + latitude = self._config[CONF_LATITUDE] + longitude = self._config[CONF_LONGITUDE] + elevation = self._config[CONF_ELEVATION] + + if not self._is_metric: + elevation = int( + round(convert_distance(elevation, LENGTH_FEET, LENGTH_METERS)) + ) + + coordinates = { + "lat": str(latitude), + "lon": str(longitude), + "msl": str(elevation), + } + + self._weather_data = metno.MetWeatherData( + coordinates, async_get_clientsession(self.hass), URL + ) + + async def fetch_data(self): + """Fetch data from API - (current weather and forecast).""" + await self._weather_data.fetching_data() + self.current_weather_data = self._weather_data.get_current_weather() + time_zone = dt_util.DEFAULT_TIME_ZONE + self.forecast_data = self._weather_data.get_forecast(time_zone) + return self diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 683390429c3..b9a992bb823 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure Met component.""" +from typing import Any, Dict, Optional + import voluptuous as vol from homeassistant import config_entries @@ -71,6 +73,12 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) + async def async_step_import( + self, user_input: Optional[Dict] = None + ) -> Dict[str, Any]: + """Handle configuration by yaml file.""" + return await self.async_step_user(user_input) + async def async_step_onboarding(self, data=None): """Handle a flow initialized by onboarding.""" return self.async_create_entry( diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index a1bcc360623..9761a2c4034 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -1,18 +1,15 @@ """Support for Met.no weather service.""" import logging -from random import randrange -import metno import voluptuous as vol from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - EVENT_CORE_CONFIG_UPDATE, - LENGTH_FEET, LENGTH_METERS, LENGTH_MILES, PRESSURE_HPA, @@ -20,13 +17,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import async_call_later from homeassistant.util.distance import convert as convert_distance -import homeassistant.util.dt as dt_util from homeassistant.util.pressure import convert as convert_pressure -from .const import CONF_TRACK_HOME +from .const import CONF_TRACK_HOME, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -36,7 +30,6 @@ ATTRIBUTION = ( ) DEFAULT_NAME = "Met.no" -URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/classic" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -62,100 +55,39 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if config.get(CONF_LATITUDE) is None: config[CONF_TRACK_HOME] = True - async_add_entities([MetWeather(config, hass.config.units.is_metric)]) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) async def async_setup_entry(hass, config_entry, async_add_entities): """Add a weather entity from a config_entry.""" - async_add_entities([MetWeather(config_entry.data, hass.config.units.is_metric)]) + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [MetWeather(coordinator, config_entry.data, hass.config.units.is_metric)] + ) class MetWeather(WeatherEntity): """Implementation of a Met.no weather condition.""" - def __init__(self, config, is_metric): + def __init__(self, coordinator, config, is_metric): """Initialise the platform with a data instance and site.""" self._config = config + self._coordinator = coordinator self._is_metric = is_metric - self._unsub_track_home = None - self._unsub_fetch_data = None - self._weather_data = None - self._current_weather_data = {} - self._coordinates = {} - self._forecast_data = None async def async_added_to_hass(self): """Start fetching data.""" - await self._init_data() - if self._config.get(CONF_TRACK_HOME): - self._unsub_track_home = self.hass.bus.async_listen( - EVENT_CORE_CONFIG_UPDATE, self._init_data - ) - - async def _init_data(self, _event=None): - """Initialize and fetch data object.""" - if self.track_home: - latitude = self.hass.config.latitude - longitude = self.hass.config.longitude - elevation = self.hass.config.elevation - else: - conf = self._config - latitude = conf[CONF_LATITUDE] - longitude = conf[CONF_LONGITUDE] - elevation = conf[CONF_ELEVATION] - - if not self._is_metric: - elevation = convert_distance(elevation, LENGTH_FEET, LENGTH_METERS) - coordinates = { - "lat": latitude, - "lon": longitude, - "msl": elevation, - } - if coordinates == self._coordinates: - return - self._coordinates = coordinates - - self._weather_data = metno.MetWeatherData( - coordinates, async_get_clientsession(self.hass), URL - ) - await self._fetch_data() - - async def will_remove_from_hass(self): - """Handle entity will be removed from hass.""" - if self._unsub_track_home: - self._unsub_track_home() - self._unsub_track_home = None - - if self._unsub_fetch_data: - self._unsub_fetch_data() - self._unsub_fetch_data = None - - async def _fetch_data(self, *_): - """Get the latest data from met.no.""" - if self._unsub_fetch_data: - self._unsub_fetch_data() - self._unsub_fetch_data = None - - if not await self._weather_data.fetching_data(): - # Retry in 15 to 20 minutes. - minutes = 15 + randrange(6) - _LOGGER.error("Retrying in %i minutes", minutes) - self._unsub_fetch_data = async_call_later( - self.hass, minutes * 60, self._fetch_data - ) - return - - # Wait between 55-65 minutes. If people update HA on the hour, this - # will make sure it will spread it out. - - self._unsub_fetch_data = async_call_later( - self.hass, randrange(55, 65) * 60, self._fetch_data + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) ) - self._current_weather_data = self._weather_data.get_current_weather() - time_zone = dt_util.DEFAULT_TIME_ZONE - self._forecast_data = self._weather_data.get_forecast(time_zone) - self.async_write_ha_state() + async def async_update(self): + """Only used by the generic entity update service.""" + await self._coordinator.async_request_refresh() @property def track_home(self): @@ -191,12 +123,12 @@ class MetWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - return self._current_weather_data.get("condition") + return self._coordinator.data.current_weather_data.get("condition") @property def temperature(self): """Return the temperature.""" - return self._current_weather_data.get("temperature") + return self._coordinator.data.current_weather_data.get("temperature") @property def temperature_unit(self): @@ -206,7 +138,7 @@ class MetWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - pressure_hpa = self._current_weather_data.get("pressure") + pressure_hpa = self._coordinator.data.current_weather_data.get("pressure") if self._is_metric or pressure_hpa is None: return pressure_hpa @@ -215,12 +147,12 @@ class MetWeather(WeatherEntity): @property def humidity(self): """Return the humidity.""" - return self._current_weather_data.get("humidity") + return self._coordinator.data.current_weather_data.get("humidity") @property def wind_speed(self): """Return the wind speed.""" - speed_m_s = self._current_weather_data.get("wind_speed") + speed_m_s = self._coordinator.data.current_weather_data.get("wind_speed") if self._is_metric or speed_m_s is None: return speed_m_s @@ -231,7 +163,7 @@ class MetWeather(WeatherEntity): @property def wind_bearing(self): """Return the wind direction.""" - return self._current_weather_data.get("wind_bearing") + return self._coordinator.data.current_weather_data.get("wind_bearing") @property def attribution(self): @@ -241,4 +173,4 @@ class MetWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return self._forecast_data + return self._coordinator.data.forecast_data diff --git a/tests/components/met/__init__.py b/tests/components/met/__init__.py index 658ed2901ec..c238fec4cb7 100644 --- a/tests/components/met/__init__.py +++ b/tests/components/met/__init__.py @@ -1 +1,26 @@ """Tests for Met.no.""" +from homeassistant.components.met.const import DOMAIN +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def init_integration(hass) -> MockConfigEntry: + """Set up the Met integration in Home Assistant.""" + entry_data = { + CONF_NAME: "test", + CONF_LATITUDE: 0, + CONF_LONGITUDE: 0, + CONF_ELEVATION: 0, + } + entry = MockConfigEntry(domain=DOMAIN, data=entry_data) + with patch( + "homeassistant.components.met.metno.MetWeatherData.fetching_data", + return_value=True, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index a4c48f38245..4d820c1c7c6 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -103,3 +103,21 @@ async def test_onboarding_step(hass): assert result["type"] == "create_entry" assert result["title"] == HOME_LOCATION_NAME assert result["data"] == {"track_home": True} + + +async def test_import_step(hass): + """Test initializing via import step.""" + test_data = { + "name": "home", + CONF_LONGITUDE: None, + CONF_LATITUDE: None, + CONF_ELEVATION: 0, + "track_home": True, + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "import"}, data=test_data + ) + + assert result["type"] == "create_entry" + assert result["title"] == "home" + assert result["data"] == test_data diff --git a/tests/components/met/test_init.py b/tests/components/met/test_init.py new file mode 100644 index 00000000000..a3323f01565 --- /dev/null +++ b/tests/components/met/test_init.py @@ -0,0 +1,19 @@ +"""Test the Met integration init.""" +from homeassistant.components.met.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED + +from . import init_integration + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_NOT_LOADED + assert not hass.data.get(DOMAIN) From 2ba4c1193c0f8702fd59e83a339f10202833f81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 9 Aug 2020 20:20:11 +0200 Subject: [PATCH 069/862] Use global CONF_UNIQUE_ID for hue (#38596) --- homeassistant/components/hue/device_trigger.py | 3 ++- homeassistant/components/hue/hue_event.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index f3a8a57167a..8a4b2eab714 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -14,10 +14,11 @@ from homeassistant.const import ( CONF_EVENT, CONF_PLATFORM, CONF_TYPE, + CONF_UNIQUE_ID, ) from . import DOMAIN -from .hue_event import CONF_HUE_EVENT, CONF_UNIQUE_ID +from .hue_event import CONF_HUE_EVENT _LOGGER = logging.getLogger(__file__) diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/hue_event.py index d3d81a6a7af..28c6ac3a594 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/hue_event.py @@ -3,7 +3,7 @@ import logging from aiohue.sensors import TYPE_ZGP_SWITCH, TYPE_ZLL_ROTARY, TYPE_ZLL_SWITCH -from homeassistant.const import CONF_EVENT, CONF_ID +from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.util import slugify @@ -13,7 +13,6 @@ _LOGGER = logging.getLogger(__name__) CONF_HUE_EVENT = "hue_event" CONF_LAST_UPDATED = "last_updated" -CONF_UNIQUE_ID = "unique_id" EVENT_NAME_FORMAT = "{}" From 3761942b827d7ea516079262868f1cdd86e62530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 9 Aug 2020 20:21:11 +0200 Subject: [PATCH 070/862] Use global CONF_UNIQUE_ID for deconz (#38597) --- homeassistant/components/deconz/deconz_event.py | 3 +-- homeassistant/components/deconz/device_trigger.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 1009ae4e54c..9ad4a7f3162 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -1,5 +1,5 @@ """Representation of a deCONZ remote.""" -from homeassistant.const import CONF_EVENT, CONF_ID +from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.util import slugify @@ -7,7 +7,6 @@ from .const import CONF_GESTURE, LOGGER from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" -CONF_UNIQUE_ID = "unique_id" class DeconzEvent(DeconzBase): diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index c343aa10fb1..f4019e98fbd 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -12,10 +12,11 @@ from homeassistant.const import ( CONF_EVENT, CONF_PLATFORM, CONF_TYPE, + CONF_UNIQUE_ID, ) from . import DOMAIN -from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE, CONF_UNIQUE_ID +from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE CONF_SUBTYPE = "subtype" From feb36c3efcaef9e788e1c69dc50c3fffe9aa1763 Mon Sep 17 00:00:00 2001 From: Alex Ward Date: Sun, 9 Aug 2020 21:15:25 +0100 Subject: [PATCH 071/862] Persist hive light brightness for color change (#38677) when changing the color for a hive light, keep the brightness at the previous level. Co-authored-by: Adam Charlton Co-authored-by: Adam Charlton --- homeassistant/components/hive/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index d6a9d1f400b..7659d43aeba 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -116,7 +116,7 @@ class HiveDeviceLight(HiveEntity, LightEntity): get_new_color = kwargs.get(ATTR_HS_COLOR) hue = int(get_new_color[0]) saturation = int(get_new_color[1]) - new_color = (hue, saturation, 100) + new_color = (hue, saturation, self.brightness) self.session.light.turn_on( self.node_id, From abb81704d2606d6c01452003a2d217167661d9c6 Mon Sep 17 00:00:00 2001 From: Arto Jantunen Date: Sun, 9 Aug 2020 23:48:38 +0300 Subject: [PATCH 072/862] Add support for boost and eco modes to Daikin climate (#37282) Daikin calls these 'econo' and 'powerful', but the result is the same.. --- homeassistant/components/daikin/climate.py | 52 ++++++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 60a126c182b..7a60cb4c3b2 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -16,6 +16,8 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, + PRESET_BOOST, + PRESET_ECO, PRESET_NONE, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, @@ -58,7 +60,12 @@ DAIKIN_TO_HA_STATE = { "off": HVAC_MODE_OFF, } -HA_PRESET_TO_DAIKIN = {PRESET_AWAY: "on", PRESET_NONE: "off"} +HA_PRESET_TO_DAIKIN = { + PRESET_AWAY: "on", + PRESET_NONE: "off", + PRESET_BOOST: "powerful", + PRESET_ECO: "econo", +} HA_ATTR_TO_DAIKIN = { ATTR_PRESET_MODE: "en_hol", @@ -70,6 +77,8 @@ HA_ATTR_TO_DAIKIN = { ATTR_TARGET_TEMPERATURE: "stemp", } +DAIKIN_ATTR_ADVANCED = "adv" + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up the Daikin HVAC platform. @@ -100,7 +109,10 @@ class DaikinClimate(ClimateEntity): self._supported_features = SUPPORT_TARGET_TEMPERATURE - if self._api.device.support_away_mode: + if ( + self._api.device.support_away_mode + or self._api.device.support_advanced_modes + ): self._supported_features |= SUPPORT_PRESET_MODE if self._api.device.support_fan_rate: @@ -227,19 +239,51 @@ class DaikinClimate(ClimateEntity): == HA_PRESET_TO_DAIKIN[PRESET_AWAY] ): return PRESET_AWAY + if ( + HA_PRESET_TO_DAIKIN[PRESET_BOOST] + in self._api.device.represent(DAIKIN_ATTR_ADVANCED)[1] + ): + return PRESET_BOOST + if ( + HA_PRESET_TO_DAIKIN[PRESET_ECO] + in self._api.device.represent(DAIKIN_ATTR_ADVANCED)[1] + ): + return PRESET_ECO return PRESET_NONE async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" if preset_mode == PRESET_AWAY: await self._api.device.set_holiday(ATTR_STATE_ON) + elif preset_mode == PRESET_BOOST: + await self._api.device.set_advanced_mode( + HA_PRESET_TO_DAIKIN[PRESET_BOOST], ATTR_STATE_ON + ) + elif preset_mode == PRESET_ECO: + await self._api.device.set_advanced_mode( + HA_PRESET_TO_DAIKIN[PRESET_ECO], ATTR_STATE_ON + ) else: - await self._api.device.set_holiday(ATTR_STATE_OFF) + if self.preset_mode == PRESET_AWAY: + await self._api.device.set_holiday(ATTR_STATE_OFF) + elif self.preset_mode == PRESET_BOOST: + await self._api.device.set_advanced_mode( + HA_PRESET_TO_DAIKIN[PRESET_BOOST], ATTR_STATE_OFF + ) + elif self.preset_mode == PRESET_ECO: + await self._api.device.set_advanced_mode( + HA_PRESET_TO_DAIKIN[PRESET_ECO], ATTR_STATE_OFF + ) @property def preset_modes(self): """List of available preset modes.""" - return list(HA_PRESET_TO_DAIKIN) + ret = [PRESET_NONE] + if self._api.device.support_away_mode: + ret.append(PRESET_AWAY) + if self._api.device.support_advanced_modes: + ret += [PRESET_ECO, PRESET_BOOST] + return ret async def async_update(self): """Retrieve latest state.""" From d7d7ee6524114ef132b8c996974825d13abb3f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 9 Aug 2020 23:00:14 +0200 Subject: [PATCH 073/862] Use global CONF_UNIQUE_ID for mqtt (#38595) * Use global CONF_UNIQUE_ID for mqtt * Update __init__.py * Update __init__.py * Update __init__.py Co-authored-by: Chris Talkington --- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mqtt/alarm_control_panel.py | 2 +- homeassistant/components/mqtt/binary_sensor.py | 2 +- homeassistant/components/mqtt/camera.py | 3 +-- homeassistant/components/mqtt/climate.py | 2 +- homeassistant/components/mqtt/cover.py | 2 +- homeassistant/components/mqtt/fan.py | 2 +- homeassistant/components/mqtt/light/schema_basic.py | 2 +- homeassistant/components/mqtt/light/schema_json.py | 2 +- homeassistant/components/mqtt/light/schema_template.py | 2 +- homeassistant/components/mqtt/lock.py | 2 +- homeassistant/components/mqtt/sensor.py | 2 +- homeassistant/components/mqtt/switch.py | 2 +- homeassistant/components/mqtt/vacuum/schema_legacy.py | 8 ++++++-- homeassistant/components/mqtt/vacuum/schema_state.py | 8 ++++++-- 15 files changed, 25 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 81c44ac8aea..73aee984ae3 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.const import CONF_UNIQUE_ID # noqa: F401 from homeassistant.core import Event, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template @@ -102,7 +103,6 @@ CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" CONF_JSON_ATTRS_TOPIC = "json_attributes_topic" CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template" -CONF_UNIQUE_ID = "unique_id" CONF_IDENTIFIERS = "identifiers" CONF_CONNECTIONS = "connections" CONF_MANUFACTURER = "manufacturer" diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index dae94b5a781..35c9d028c52 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_CODE, CONF_DEVICE, CONF_NAME, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -38,7 +39,6 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 5d69bfde4f6..9afb9401bfe 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import callback @@ -30,7 +31,6 @@ from . import ( ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 1bfb248d94a..d0311d5c694 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components import camera, mqtt from homeassistant.components.camera import Camera -from homeassistant.const import CONF_DEVICE, CONF_NAME +from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -14,7 +14,6 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( ATTR_DISCOVERY_HASH, CONF_QOS, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 8cc6a3ffbdb..3bd8aae9239 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -38,6 +38,7 @@ from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_TEMPERATURE_UNIT, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, PRECISION_HALVES, PRECISION_TENTHS, @@ -53,7 +54,6 @@ from . import ( ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, - CONF_UNIQUE_ID, MQTT_BASE_PLATFORM_SCHEMA, MqttAttributes, MqttAvailability, diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index c9fc388e1a3..772c2900eed 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_NAME, CONF_OPTIMISTIC, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_CLOSED, STATE_CLOSING, @@ -41,7 +42,6 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 9ce3809bfe8..e70ff62b0fb 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -21,6 +21,7 @@ from homeassistant.const import ( CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_STATE, + CONF_UNIQUE_ID, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -33,7 +34,6 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 375318764d1..03b0646307f 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -22,7 +22,6 @@ from homeassistant.components.mqtt import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -41,6 +40,7 @@ from homeassistant.const import ( CONF_PAYLOAD_ON, CONF_RGB, CONF_STATE, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY, diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 89ae0601fde..4c8b9c41405 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -29,7 +29,6 @@ from homeassistant.components.mqtt import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -44,6 +43,7 @@ from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, + CONF_UNIQUE_ID, CONF_WHITE_VALUE, CONF_XY, STATE_ON, diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index d14cda70bb6..0c512f0e1a3 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -26,7 +26,6 @@ from homeassistant.components.mqtt import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -37,6 +36,7 @@ from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, + CONF_UNIQUE_ID, STATE_OFF, STATE_ON, ) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 34905fe8aa4..744eeb17e6f 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -9,6 +9,7 @@ from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import callback @@ -22,7 +23,6 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 3ad58468cab..f2cb8f22e84 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_ICON, CONF_NAME, + CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) @@ -28,7 +29,6 @@ from . import ( ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 44d028829d0..8a164019f10 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_ON, ) @@ -27,7 +28,6 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index b69565f7114..907e2e4a08f 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.mqtt import ( - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -27,7 +26,12 @@ from homeassistant.components.vacuum import ( SUPPORT_TURN_ON, VacuumEntity, ) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_DEVICE, CONF_NAME +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + CONF_DEVICE, + CONF_NAME, + CONF_UNIQUE_ID, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 628f85614fe..9f75f38f1bc 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -10,7 +10,6 @@ from homeassistant.components.mqtt import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -36,7 +35,12 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_DEVICE, CONF_NAME +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + CONF_DEVICE, + CONF_NAME, + CONF_UNIQUE_ID, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv From 3d308e0599a4c968bf19853ff6e33302f6cc623a Mon Sep 17 00:00:00 2001 From: Markus Haack Date: Sun, 9 Aug 2020 23:28:45 +0200 Subject: [PATCH 074/862] Add SolarEdge battery level and dynamic icon for storage sensor (#37826) * Add SolarEdge battery level and dynamic icon for storage sensor * Add SolarEdge battery storage sensor * Fix isort warning * Remove charging attribute * Fix isort warning * Apply suggestions from code review * Update homeassistant/components/solaredge/sensor.py Co-authored-by: Chris Talkington --- homeassistant/components/solaredge/const.py | 3 +- homeassistant/components/solaredge/sensor.py | 32 +++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 9acdc43f3c7..0749cb54827 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -1,7 +1,7 @@ """Constants for the SolarEdge Monitoring API.""" from datetime import timedelta -from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT +from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT, UNIT_PERCENTAGE DOMAIN = "solaredge" @@ -77,4 +77,5 @@ SENSOR_TYPES = { False, ], "feedin_power": ["FeedIn", "Exported Power", None, "mdi:flash", False], + "storage_level": ["STORAGE", "Storage Level", UNIT_PERCENTAGE, None, False], } diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 469f8ef64a2..3888b8bf536 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -6,7 +6,7 @@ from requests.exceptions import ConnectTimeout, HTTPError import solaredge from stringcase import snakecase -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -83,6 +83,9 @@ class SolarEdgeSensorFactory: for key in ["power_consumption", "solar_power", "grid_power", "storage_power"]: self.services[key] = (SolarEdgePowerFlowSensor, flow) + for key in ["storage_level"]: + self.services[key] = (SolarEdgeStorageLevelSensor, flow) + for key in [ "purchased_power", "production_power", @@ -233,6 +236,11 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensor): """Return the state attributes.""" return self._attributes + @property + def device_class(self): + """Device Class.""" + return DEVICE_CLASS_POWER + def update(self): """Get the latest inventory data and update state and attributes.""" self.data_service.update() @@ -241,6 +249,27 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensor): self._unit_of_measurement = self.data_service.unit +class SolarEdgeStorageLevelSensor(SolarEdgeSensor): + """Representation of an SolarEdge Monitoring API storage level sensor.""" + + def __init__(self, platform_name, sensor_key, data_service): + """Initialize the storage level sensor.""" + super().__init__(platform_name, sensor_key, data_service) + + self._json_key = SENSOR_TYPES[self.sensor_key][0] + + @property + def device_class(self): + """Return the device_class of the device.""" + return DEVICE_CLASS_BATTERY + + def update(self): + """Get the latest inventory data and update state and attributes.""" + self.data_service.update() + attr = self.data_service.attributes.get(self._json_key) + self._state = attr["soc"] + + class SolarEdgeDataService: """Get and update the latest data.""" @@ -470,6 +499,7 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService): charge = key.lower() in power_to self.data[key] *= -1 if charge else 1 self.attributes[key]["flow"] = "charge" if charge else "discharge" + self.attributes[key]["soc"] = value["chargeLevel"] _LOGGER.debug( "Updated SolarEdge power flow: %s, %s", self.data, self.attributes From 4e56339ba164308e61b9370130b1685a9b01857c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 9 Aug 2020 20:37:07 -0400 Subject: [PATCH 075/862] add event and device action for when devices drop (#38701) --- homeassistant/components/zha/core/device.py | 18 +++- homeassistant/components/zha/strings.json | 3 +- tests/components/zha/test_device_trigger.py | 100 +++++++++++++++++++- 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 53b1dcc163b..b8229793a48 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -30,6 +30,7 @@ from .const import ( ATTR_CLUSTER_ID, ATTR_COMMAND, ATTR_COMMAND_TYPE, + ATTR_DEVICE_IEEE, ATTR_DEVICE_TYPE, ATTR_ENDPOINT_ID, ATTR_ENDPOINTS, @@ -247,9 +248,14 @@ class ZHADevice(LogMixin): @property def device_automation_triggers(self): """Return the device automation triggers for this device.""" + triggers = { + ("device_offline", "device_offline"): { + "device_event_type": "device_offline" + } + } if hasattr(self._zigpy_device, "device_automation_triggers"): - return self._zigpy_device.device_automation_triggers - return None + triggers.update(self._zigpy_device.device_automation_triggers) + return triggers @property def available_signal(self): @@ -346,6 +352,14 @@ class ZHADevice(LogMixin): # reinit channels then signal entities self.hass.async_create_task(self._async_became_available()) return + if availability_changed and not available: + self.hass.bus.async_fire( + "zha_event", + { + ATTR_DEVICE_IEEE: str(self.ieee), + "device_event_type": "device_offline", + }, + ) async_dispatcher_send(self.hass, f"{self._available_signal}_entity") async def _async_became_available(self) -> None: diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index b26cebbd40a..915501b2437 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -51,7 +51,8 @@ "device_tilted": "Device tilted", "device_knocked": "Device knocked \"{subtype}\"", "device_dropped": "Device dropped", - "device_flipped": "Device flipped \"{subtype}\"" + "device_flipped": "Device flipped \"{subtype}\"", + "device_offline": "Device offline" }, "trigger_subtype": { "turn_on": "Turn on", diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 266094963f2..e0a73903e0d 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,12 +1,23 @@ """ZHA device automation trigger tests.""" +from datetime import timedelta +import time + import pytest import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation +import homeassistant.components.zha.core.device as zha_core_device from homeassistant.helpers.device_registry import async_get_registry from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -from tests.common import async_get_device_automations, async_mock_service +from .common import async_enable_traffic + +from tests.common import ( + async_fire_time_changed, + async_get_device_automations, + async_mock_service, +) ON = 1 OFF = 0 @@ -49,7 +60,7 @@ async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored): "out_clusters": [general.OnOff.cluster_id], "device_type": 0, } - }, + } ) zha_device = await zha_device_joined_restored(zigpy_device) @@ -79,6 +90,13 @@ async def test_triggers(hass, mock_devices): triggers = await async_get_device_automations(hass, "trigger", reg_device.id) expected_triggers = [ + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "device_offline", + "subtype": "device_offline", + }, { "device_id": reg_device.id, "domain": "zha", @@ -128,7 +146,15 @@ async def test_no_triggers(hass, mock_devices): reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) triggers = await async_get_device_automations(hass, "trigger", reg_device.id) - assert triggers == [] + assert triggers == [ + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "device_offline", + "subtype": "device_offline", + } + ] async def test_if_fires_on_event(hass, mock_devices, calls): @@ -180,6 +206,74 @@ async def test_if_fires_on_event(hass, mock_devices, calls): assert calls[0].data["message"] == "service called" +async def test_device_offline_fires( + hass, zigpy_device_mock, zha_device_restored, calls +): + """Test for device offline triggers firing.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [general.Basic.cluster_id], + "out_clusters": [general.OnOff.cluster_id], + "device_type": 0, + } + } + ) + + zha_device = await zha_device_restored(zigpy_device, last_seen=time.time()) + await async_enable_traffic(hass, [zha_device]) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": zha_device.device_id, + "domain": "zha", + "platform": "device", + "type": "device_offline", + "subtype": "device_offline", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + + await hass.async_block_till_done() + assert zha_device.available is True + + zigpy_device.last_seen = ( + time.time() - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2 + ) + + # there are 3 checkins to perform before marking the device unavailable + future = dt_util.utcnow() + timedelta(seconds=90) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + future = dt_util.utcnow() + timedelta(seconds=90) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + future = dt_util.utcnow() + timedelta( + seconds=zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 100 + ) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert zha_device.available is False + assert len(calls) == 1 + assert calls[0].data["message"] == "service called" + + async def test_exception_no_triggers(hass, mock_devices, calls, caplog): """Test for exception on event triggers firing.""" From 9bb7b3b1255f4ee827668784a509f5d13f10c5b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Aug 2020 22:17:13 -0500 Subject: [PATCH 076/862] Fix homekit_controller pairing retry when the first attempt is busy (#38605) * Fix homekit_controller pairing retry If the device was busy on the first pairing attempt, it was not possible to retry. * always restart pairing on recoverable execptions * move code * malformed pin is safe to restart * make busy_error an abort * switch max retries, simplify tests * try pairing later * try pairing later * merge * s/tlv_error/protocol_error/g * Adjust wording --- .../homekit_controller/config_flow.py | 66 ++++++++++------ .../homekit_controller/strings.json | 7 +- .../homekit_controller/translations/en.json | 79 ++++++++++--------- .../homekit_controller/test_config_flow.py | 48 ++++++++++- 4 files changed, 137 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 4f704d7ea59..1cd4a0b4c50 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -238,10 +238,42 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # in. errors = {} + if self.controller is None: await self._async_setup_controller() - if pair_info: + if not self.finish_pairing: + # Its possible that the first try may have been busy so + # we always check to see if self.finish_paring has been + # set. + discovery = await self.controller.find_ip_by_device_id(self.hkid) + + try: + self.finish_pairing = await discovery.start_pairing(self.hkid) + + except aiohomekit.BusyError: + # Already performing a pair setup operation with a different + # controller + errors["base"] = "busy_error" + except aiohomekit.MaxTriesError: + # The accessory has received more than 100 unsuccessful auth + # attempts. + errors["base"] = "max_tries_error" + except aiohomekit.UnavailableError: + # The accessory is already paired - cannot try to pair again. + return self.async_abort(reason="already_paired") + except aiohomekit.AccessoryNotFoundError: + # Can no longer find the device on the network + return self.async_abort(reason="accessory_not_found_error") + except IndexError: + # TLV error, usually not in pairing mode + _LOGGER.exception("Pairing communication failed") + errors["base"] = "protocol_error" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Pairing attempt failed with an unhandled exception") + errors["pairing_code"] = "pairing_failed" + + if pair_info and self.finish_pairing: code = pair_info["pairing_code"] try: code = ensure_pin_format(code) @@ -257,45 +289,33 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # PairVerify M4 - Device not recognised # PairVerify M4 - Ed25519 signature verification failed errors["pairing_code"] = "authentication_error" + self.finish_pairing = None except aiohomekit.UnknownError: # An error occurred on the device whilst performing this # operation. errors["pairing_code"] = "unknown_error" + self.finish_pairing = None except aiohomekit.MaxPeersError: # The device can't pair with any more accessories. errors["pairing_code"] = "max_peers_error" + self.finish_pairing = None except aiohomekit.AccessoryNotFoundError: # Can no longer find the device on the network return self.async_abort(reason="accessory_not_found_error") except Exception: # pylint: disable=broad-except _LOGGER.exception("Pairing attempt failed with an unhandled exception") + self.finish_pairing = None errors["pairing_code"] = "pairing_failed" - discovery = await self.controller.find_ip_by_device_id(self.hkid) - - try: - self.finish_pairing = await discovery.start_pairing(self.hkid) - - except aiohomekit.BusyError: - # Already performing a pair setup operation with a different - # controller - errors["pairing_code"] = "busy_error" - except aiohomekit.MaxTriesError: - # The accessory has received more than 100 unsuccessful auth - # attempts. - errors["pairing_code"] = "max_tries_error" - except aiohomekit.UnavailableError: - # The accessory is already paired - cannot try to pair again. - return self.async_abort(reason="already_paired") - except aiohomekit.AccessoryNotFoundError: - # Can no longer find the device on the network - return self.async_abort(reason="accessory_not_found_error") - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Pairing attempt failed with an unhandled exception") - errors["pairing_code"] = "pairing_failed" + if errors and "base" in errors: + return self.async_show_form(step_id="try_pair_later", errors=errors) return self._async_step_pair_show_form(errors) + async def async_step_try_pair_later(self, pair_info=None): + """Retry pairing after the accessory is busy or unavailable.""" + return await self.async_step_pair(pair_info) + @callback def _async_step_pair_show_form(self, errors=None): return self.async_show_form( diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 118c3bf7f8a..6be751e63c9 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -16,11 +16,16 @@ "data": { "pairing_code": "Pairing Code" } - } + }, + "try_pair_later": { + "title": "Pairing Unavailable", + "description": "Ensure the device is in pairing mode or try restarting the device, then continue to re-start pairing." + } }, "error": { "unable_to_pair": "Unable to pair, please try again.", "unknown_error": "Device reported an unknown error. Pairing failed.", + "protocol_error": "Error communicating with the accessory. Device may not be in pairing mode and may require a physical or virtual button press.", "authentication_error": "Incorrect HomeKit code. Please check it and try again.", "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", "busy_error": "Device refused to add pairing as it is already pairing with another controller.", diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 69ea4c3c351..6be751e63c9 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -1,40 +1,45 @@ { - "config": { - "abort": { - "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", - "already_configured": "Accessory is already configured with this controller.", - "already_in_progress": "Config flow for device is already in progress.", - "already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.", - "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", - "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", - "no_devices": "No unpaired devices could be found" - }, - "error": { - "authentication_error": "Incorrect HomeKit code. Please check it and try again.", - "busy_error": "Device refused to add pairing as it is already pairing with another controller.", - "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", - "max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.", - "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.", - "unable_to_pair": "Unable to pair, please try again.", - "unknown_error": "Device reported an unknown error. Pairing failed." - }, - "flow_title": "HomeKit Accessory: {name}", - "step": { - "pair": { - "data": { - "pairing_code": "Pairing Code" - }, - "description": "Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory", - "title": "Pair with HomeKit Accessory" - }, - "user": { - "data": { - "device": "Device" - }, - "description": "Select the device you want to pair with", - "title": "Pair with HomeKit Accessory" - } + "title": "HomeKit Controller", + "config": { + "flow_title": "HomeKit Accessory: {name}", + "step": { + "user": { + "title": "Pair with HomeKit Accessory", + "description": "Select the device you want to pair with", + "data": { + "device": "Device" } + }, + "pair": { + "title": "Pair with HomeKit Accessory", + "description": "Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory", + "data": { + "pairing_code": "Pairing Code" + } + }, + "try_pair_later": { + "title": "Pairing Unavailable", + "description": "Ensure the device is in pairing mode or try restarting the device, then continue to re-start pairing." + } }, - "title": "HomeKit Controller" -} \ No newline at end of file + "error": { + "unable_to_pair": "Unable to pair, please try again.", + "unknown_error": "Device reported an unknown error. Pairing failed.", + "protocol_error": "Error communicating with the accessory. Device may not be in pairing mode and may require a physical or virtual button press.", + "authentication_error": "Incorrect HomeKit code. Please check it and try again.", + "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", + "busy_error": "Device refused to add pairing as it is already pairing with another controller.", + "max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.", + "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently." + }, + "abort": { + "no_devices": "No unpaired devices could be found", + "already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.", + "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", + "already_configured": "Accessory is already configured with this controller.", + "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", + "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", + "already_in_progress": "Config flow for device is already in progress." + } + } +} diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index a9aef723164..caab127aa87 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -14,8 +14,6 @@ from tests.async_mock import patch from tests.common import MockConfigEntry PAIRING_START_FORM_ERRORS = [ - (aiohomekit.BusyError, "busy_error"), - (aiohomekit.MaxTriesError, "max_tries_error"), (KeyError, "pairing_failed"), ] @@ -24,6 +22,12 @@ PAIRING_START_ABORT_ERRORS = [ (aiohomekit.UnavailableError, "already_paired"), ] +PAIRING_TRY_LATER_ERRORS = [ + (aiohomekit.BusyError, "busy_error"), + (aiohomekit.MaxTriesError, "max_tries_error"), + (IndexError, "protocol_error"), +] + PAIRING_FINISH_FORM_ERRORS = [ (aiohomekit.exceptions.MalformedPinError, "authentication_error"), (aiohomekit.MaxPeersError, "max_peers_error"), @@ -314,6 +318,39 @@ async def test_pair_abort_errors_on_start(hass, controller, exception, expected) assert result["reason"] == expected +@pytest.mark.parametrize("exception,expected", PAIRING_TRY_LATER_ERRORS) +async def test_pair_try_later_errors_on_start(hass, controller, exception, expected): + """Test various pairing errors.""" + + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + + # User initiates pairing - device refuses to enter pairing mode but may be successful after entering pairing mode or rebooting + test_exc = exception("error") + with patch.object(device, "start_pairing", side_effect=test_exc): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2["step_id"] == "try_pair_later" + assert result2["type"] == "form" + assert result2["errors"]["base"] == expected + + # Device is rebooted or placed into pairing mode as they have been instructed + + # We start pairing again + result3 = await hass.config_entries.flow.async_configure(result2["flow_id"]) + + # .. and successfully complete pair + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], user_input={"pairing_code": "111-22-333"} + ) + assert result4["type"] == "create_entry" + assert result4["title"] == "Koogeek-LS1-20833F" + + @pytest.mark.parametrize("exception,expected", PAIRING_START_FORM_ERRORS) async def test_pair_form_errors_on_start(hass, controller, exception, expected): """Test various pairing errors.""" @@ -347,6 +384,13 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): "source": "zeroconf", } + # User re-tries entering pairing code + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pairing_code": "111-22-333"} + ) + assert result["type"] == "create_entry" + assert result["title"] == "Koogeek-LS1-20833F" + @pytest.mark.parametrize("exception,expected", PAIRING_FINISH_ABORT_ERRORS) async def test_pair_abort_errors_on_finish(hass, controller, exception, expected): From 4c8c8a8c999afc1048349d2627c4aef85b812b50 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sun, 9 Aug 2020 22:26:35 -0500 Subject: [PATCH 077/862] Fix async_fire_time_changed in tests/common.py (#38705) --- tests/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index bcb66428f6b..16f349de800 100644 --- a/tests/common.py +++ b/tests/common.py @@ -295,8 +295,8 @@ def async_fire_time_changed(hass, datetime_, fire_all=False): if task.cancelled(): continue - future_seconds = task.when() - hass.loop.time() mock_seconds_into_future = datetime_.timestamp() - time.time() + future_seconds = task.when() - hass.loop.time() if fire_all or mock_seconds_into_future >= future_seconds: with patch( From 65f1e0c71a5c2438f89689737499a1a95f58fdb3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 10 Aug 2020 13:12:23 +0200 Subject: [PATCH 078/862] Update base image 8.2.1 (#38716) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index 279a58f4c5a..a1db6ac2a54 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:8.1.0", - "armhf": "homeassistant/armhf-homeassistant-base:8.1.0", - "armv7": "homeassistant/armv7-homeassistant-base:8.1.0", - "amd64": "homeassistant/amd64-homeassistant-base:8.1.0", - "i386": "homeassistant/i386-homeassistant-base:8.1.0" + "aarch64": "homeassistant/aarch64-homeassistant-base:8.2.1", + "armhf": "homeassistant/armhf-homeassistant-base:8.2.1", + "armv7": "homeassistant/armv7-homeassistant-base:8.2.1", + "amd64": "homeassistant/amd64-homeassistant-base:8.2.1", + "i386": "homeassistant/i386-homeassistant-base:8.2.1" }, "labels": { "io.hass.type": "core" From 07de9deab62a94722ada334b0979cd398ab37ba9 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Mon, 10 Aug 2020 13:34:18 +0200 Subject: [PATCH 079/862] Implement local discovery of Smappee legacy devices (#37812) Co-authored-by: Martin Hjelmare --- homeassistant/components/smappee/__init__.py | 40 +- .../components/smappee/binary_sensor.py | 8 +- .../components/smappee/config_flow.py | 156 ++++++- homeassistant/components/smappee/const.py | 9 +- .../components/smappee/manifest.json | 5 +- homeassistant/components/smappee/sensor.py | 46 +- homeassistant/components/smappee/strings.json | 23 +- homeassistant/components/smappee/switch.py | 4 +- .../components/smappee/translations/en.json | 31 +- homeassistant/generated/zeroconf.py | 3 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smappee/test_config_flow.py | 407 +++++++++++++++++- tests/components/smappee/test_init.py | 32 ++ 14 files changed, 708 insertions(+), 60 deletions(-) create mode 100644 tests/components/smappee/test_init.py diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 381678f0e86..3820863de93 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -5,7 +5,12 @@ from pysmappee import Smappee import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_PLATFORM +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_IP_ADDRESS, + CONF_PLATFORM, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.util import Throttle @@ -13,7 +18,7 @@ from homeassistant.util import Throttle from . import api, config_flow from .const import ( AUTHORIZE_URL, - BASE, + CONF_SERIALNUMBER, DOMAIN, MIN_TIME_BETWEEN_UPDATES, SMAPPEE_PLATFORMS, @@ -40,11 +45,14 @@ async def async_setup(hass: HomeAssistant, config: dict): if DOMAIN not in config: return True + client_id = config[DOMAIN][CONF_CLIENT_ID] + hass.data[DOMAIN][client_id] = {} + # decide platform platform = "PRODUCTION" - if config[DOMAIN][CONF_CLIENT_ID] == "homeassistant_f2": + if client_id == "homeassistant_f2": platform = "ACCEPTANCE" - elif config[DOMAIN][CONF_CLIENT_ID] == "homeassistant_f3": + elif client_id == "homeassistant_f3": platform = "DEVELOPMENT" hass.data[DOMAIN][CONF_PLATFORM] = platform @@ -65,17 +73,22 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Smappee from a config entry.""" - implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry - ) + """Set up Smappee from a zeroconf or config entry.""" + if CONF_IP_ADDRESS in entry.data: + smappee_api = api.api.SmappeeLocalApi(ip=entry.data[CONF_IP_ADDRESS]) + smappee = Smappee(api=smappee_api, serialnumber=entry.data[CONF_SERIALNUMBER]) + await hass.async_add_executor_job(smappee.load_local_service_location) + else: + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) - smappee_api = api.ConfigEntrySmappeeApi(hass, entry, implementation) + smappee_api = api.ConfigEntrySmappeeApi(hass, entry, implementation) - smappee = Smappee(smappee_api) - await hass.async_add_executor_job(smappee.load_service_locations) + smappee = Smappee(api=smappee_api) + await hass.async_add_executor_job(smappee.load_service_locations) - hass.data[DOMAIN][BASE] = SmappeeBase(hass, smappee) + hass.data[DOMAIN][entry.entry_id] = SmappeeBase(hass, smappee) for component in SMAPPEE_PLATFORMS: hass.async_create_task( @@ -97,8 +110,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) + hass.data[DOMAIN].pop(entry.entry_id, None) return unload_ok diff --git a/homeassistant/components/smappee/binary_sensor.py b/homeassistant/components/smappee/binary_sensor.py index 9b55f358ef3..fbd0c89f4b6 100644 --- a/homeassistant/components/smappee/binary_sensor.py +++ b/homeassistant/components/smappee/binary_sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorEntity -from .const import BASE, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -13,7 +13,7 @@ PRESENCE_PREFIX = "Presence" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Smappee binary sensor.""" - smappee_base = hass.data[DOMAIN][BASE] + smappee_base = hass.data[DOMAIN][config_entry.entry_id] entities = [] for service_location in smappee_base.smappee.service_locations.values(): @@ -29,7 +29,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - entities.append(SmappeePresence(smappee_base, service_location)) + if not smappee_base.smappee.local_polling: + # presence value only available in cloud env + entities.append(SmappeePresence(smappee_base, service_location)) async_add_entities(entities, True) diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index e07c3b65e37..32f74aa9736 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -1,10 +1,14 @@ """Config flow for Smappee.""" import logging +import voluptuous as vol + from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS from homeassistant.helpers import config_entry_oauth2_flow -from .const import DOMAIN # pylint: disable=unused-import +from . import api +from .const import CONF_HOSTNAME, CONF_SERIALNUMBER, DOMAIN, ENV_CLOUD, ENV_LOCAL _LOGGER = logging.getLogger(__name__) @@ -12,12 +16,160 @@ _LOGGER = logging.getLogger(__name__) class SmappeeFlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN ): - """Config flow to handle Smappee OAuth2 authentication.""" + """Config Smappee config flow.""" DOMAIN = DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + async def async_oauth_create_entry(self, data): + """Create an entry for the flow.""" + + await self.async_set_unique_id(unique_id=f"{DOMAIN}Cloud") + return self.async_create_entry(title=f"{DOMAIN}Cloud", data=data) + @property def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) + + async def async_step_zeroconf(self, discovery_info): + """Handle zeroconf discovery.""" + + if not discovery_info[CONF_HOSTNAME].startswith("Smappee1"): + # We currently only support Energy and Solar models (legacy) + return self.async_abort(reason="invalid_mdns") + + serial_number = ( + discovery_info[CONF_HOSTNAME].replace(".local.", "").replace("Smappee", "") + ) + + # Check if already configured (local) + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() + + # Check if already configured (cloud) + if self.is_cloud_device_already_added(): + return self.async_abort(reason="already_configured_device") + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update( + { + CONF_IP_ADDRESS: discovery_info["host"], + CONF_SERIALNUMBER: serial_number, + "title_placeholders": {"name": serial_number}, + } + ) + + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm(self, user_input=None): + """Confirm zeroconf flow.""" + errors = {} + + # Check if already configured (cloud) + if self.is_cloud_device_already_added(): + return self.async_abort(reason="already_configured_device") + + if user_input is None: + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + serialnumber = self.context.get(CONF_SERIALNUMBER) + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"serialnumber": serialnumber}, + errors=errors, + ) + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + ip_address = self.context.get(CONF_IP_ADDRESS) + serial_number = self.context.get(CONF_SERIALNUMBER) + + # Attempt to make a connection to the local device + smappee_api = api.api.SmappeeLocalApi(ip=ip_address) + logon = await self.hass.async_add_executor_job(smappee_api.logon) + if logon is None: + return self.async_abort(reason="connection_error") + + return self.async_create_entry( + title=f"{DOMAIN}{serial_number}", + data={CONF_IP_ADDRESS: ip_address, CONF_SERIALNUMBER: serial_number}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + + # If there is a CLOUD entry already, abort a new LOCAL entry + if self.is_cloud_device_already_added(): + return self.async_abort(reason="already_configured_device") + + return await self.async_step_environment() + + async def async_step_environment(self, user_input=None): + """Decide environment, cloud or local.""" + if user_input is None: + return self.async_show_form( + step_id="environment", + data_schema=vol.Schema( + { + vol.Required("environment", default=ENV_CLOUD): vol.In( + [ENV_CLOUD, ENV_LOCAL] + ) + } + ), + errors={}, + ) + + # Environment chosen, request additional host information for LOCAL or OAuth2 flow for CLOUD + # Ask for host detail + if user_input["environment"] == ENV_LOCAL: + return await self.async_step_local() + + # Abort cloud option if a LOCAL entry has already been added + if user_input["environment"] == ENV_CLOUD and self._async_current_entries(): + return self.async_abort(reason="already_configured_local_device") + + return await self.async_step_pick_implementation() + + async def async_step_local(self, user_input=None): + """Handle local flow.""" + if user_input is None: + return self.async_show_form( + step_id="local", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors={}, + ) + # In a LOCAL setup we still need to resolve the host to serial number + ip_address = user_input["host"] + smappee_api = api.api.SmappeeLocalApi(ip=ip_address) + logon = await self.hass.async_add_executor_job(smappee_api.logon) + if logon is None: + return self.async_abort(reason="connection_error") + + advanced_config = await self.hass.async_add_executor_job( + smappee_api.load_advanced_config + ) + serial_number = None + for config_item in advanced_config: + if config_item["key"] == "mdnsHostName": + serial_number = config_item["value"] + + if serial_number is None or not serial_number.startswith("Smappee1"): + # We currently only support Energy and Solar models (legacy) + return self.async_abort(reason="invalid_mdns") + + serial_number = serial_number.replace("Smappee", "") + + # Check if already configured (local) + await self.async_set_unique_id(serial_number, raise_on_progress=False) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=f"{DOMAIN}{serial_number}", + data={CONF_IP_ADDRESS: ip_address, CONF_SERIALNUMBER: serial_number}, + ) + + def is_cloud_device_already_added(self): + """Check if a CLOUD device has already been added.""" + for entry in self._async_current_entries(): + if entry.unique_id is not None and entry.unique_id == f"{DOMAIN}Cloud": + return True + return False diff --git a/homeassistant/components/smappee/const.py b/homeassistant/components/smappee/const.py index 4bc370e9c09..531327b8369 100644 --- a/homeassistant/components/smappee/const.py +++ b/homeassistant/components/smappee/const.py @@ -5,11 +5,16 @@ from datetime import timedelta DOMAIN = "smappee" DATA_CLIENT = "smappee_data" -BASE = "BASE" +CONF_HOSTNAME = "hostname" +CONF_SERIALNUMBER = "serialnumber" +CONF_TITLE = "title" + +ENV_CLOUD = "cloud" +ENV_LOCAL = "local" SMAPPEE_PLATFORMS = ["binary_sensor", "sensor", "switch"] -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=20) AUTHORIZE_URL = { "PRODUCTION": "https://app1pub.smappee.net/dev/v1/oauth2/authorize", diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 4f9a33b74da..fe0b0de281c 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,9 +5,12 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.1.5" + "pysmappee==0.2.9" ], "codeowners": [ "@bsmappee" + ], + "zeroconf": [ + "_ssh._tcp.local." ] } diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 1b0b5af8564..cffdb7c5024 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -4,7 +4,7 @@ import logging from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_WATT_HOUR, POWER_WATT, VOLT from homeassistant.helpers.entity import Entity -from .const import BASE, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -15,6 +15,7 @@ TREND_SENSORS = { POWER_WATT, "total_power", DEVICE_CLASS_POWER, + True, # both cloud and local ], "alwayson": [ "Always on - Active power", @@ -22,6 +23,7 @@ TREND_SENSORS = { POWER_WATT, "alwayson", DEVICE_CLASS_POWER, + False, # cloud only ], "power_today": [ "Total consumption - Today", @@ -29,6 +31,7 @@ TREND_SENSORS = { ENERGY_WATT_HOUR, "power_today", None, + False, # cloud only ], "power_current_hour": [ "Total consumption - Current hour", @@ -36,6 +39,7 @@ TREND_SENSORS = { ENERGY_WATT_HOUR, "power_current_hour", None, + False, # cloud only ], "power_last_5_minutes": [ "Total consumption - Last 5 minutes", @@ -43,6 +47,7 @@ TREND_SENSORS = { ENERGY_WATT_HOUR, "power_last_5_minutes", None, + False, # cloud only ], "alwayson_today": [ "Always on - Today", @@ -50,6 +55,7 @@ TREND_SENSORS = { ENERGY_WATT_HOUR, "alwayson_today", None, + False, # cloud only ], } REACTIVE_SENSORS = { @@ -68,6 +74,7 @@ SOLAR_SENSORS = { POWER_WATT, "solar_power", DEVICE_CLASS_POWER, + True, # both cloud and local ], "solar_today": [ "Total production - Today", @@ -75,6 +82,7 @@ SOLAR_SENSORS = { ENERGY_WATT_HOUR, "solar_today", None, + False, # cloud only ], "solar_current_hour": [ "Total production - Current hour", @@ -82,6 +90,7 @@ SOLAR_SENSORS = { ENERGY_WATT_HOUR, "solar_current_hour", None, + False, # cloud only ], } VOLTAGE_SENSORS = { @@ -138,20 +147,22 @@ VOLTAGE_SENSORS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Smappee sensor.""" - smappee_base = hass.data[DOMAIN][BASE] + smappee_base = hass.data[DOMAIN][config_entry.entry_id] entities = [] for service_location in smappee_base.smappee.service_locations.values(): # Add all basic sensors (realtime values and aggregators) + # Some are available in local only env for sensor in TREND_SENSORS: - entities.append( - SmappeeSensor( - smappee_base=smappee_base, - service_location=service_location, - sensor=sensor, - attributes=TREND_SENSORS[sensor], + if not service_location.local_polling or TREND_SENSORS[sensor][5]: + entities.append( + SmappeeSensor( + smappee_base=smappee_base, + service_location=service_location, + sensor=sensor, + attributes=TREND_SENSORS[sensor], + ) ) - ) if service_location.has_reactive_value: for reactive_sensor in REACTIVE_SENSORS: @@ -164,17 +175,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - # Add solar sensors + # Add solar sensors (some are available in local only env) if service_location.has_solar_production: for sensor in SOLAR_SENSORS: - entities.append( - SmappeeSensor( - smappee_base=smappee_base, - service_location=service_location, - sensor=sensor, - attributes=SOLAR_SENSORS[sensor], + if not service_location.local_polling or SOLAR_SENSORS[sensor][5]: + entities.append( + SmappeeSensor( + smappee_base=smappee_base, + service_location=service_location, + sensor=sensor, + attributes=SOLAR_SENSORS[sensor], + ) ) - ) # Add all CT measurements for measurement_id, measurement in service_location.measurements.items(): diff --git a/homeassistant/components/smappee/strings.json b/homeassistant/components/smappee/strings.json index 6b86bd042ac..9d4bb618832 100644 --- a/homeassistant/components/smappee/strings.json +++ b/homeassistant/components/smappee/strings.json @@ -1,13 +1,34 @@ { "config": { + "flow_title": "Smappee: {name}", "step": { + "environment": { + "description": "Set up your Smappee to integrate with Home Assistant.", + "data": { + "environment": "Environment" + } + }, + "local": { + "description": "Enter the host to initiate the Smappee local integration", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?", + "title": "Discovered Smappee device" + }, "pick_implementation": { "title": "Pick Authentication Method" } }, "abort": { + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The component is not configured. Please follow the documentation." + "connection_error": "Failed to connect to Smappee device.", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "invalid_mdns": "Unsupported device for the Smappee integration." } } } diff --git a/homeassistant/components/smappee/switch.py b/homeassistant/components/smappee/switch.py index 7d6a7f2405f..a845386e71c 100644 --- a/homeassistant/components/smappee/switch.py +++ b/homeassistant/components/smappee/switch.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.switch import SwitchEntity -from .const import BASE, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -15,7 +15,7 @@ SCAN_INTERVAL = timedelta(seconds=5) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Smappee Comfort Plugs.""" - smappee_base = hass.data[DOMAIN][BASE] + smappee_base = hass.data[DOMAIN][config_entry.entry_id] entities = [] for service_location in smappee_base.smappee.service_locations.values(): diff --git a/homeassistant/components/smappee/translations/en.json b/homeassistant/components/smappee/translations/en.json index 505d56e73a0..36d8d3a8f35 100644 --- a/homeassistant/components/smappee/translations/en.json +++ b/homeassistant/components/smappee/translations/en.json @@ -1,13 +1,34 @@ { "config": { - "abort": { - "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The component is not configured. Please follow the documentation." - }, + "flow_title": "Smappee: {name}", "step": { + "environment": { + "description": "Set up your Smappee to integrate with Home Assistant.", + "data": { + "environment": "Environment" + } + }, + "local": { + "description": "Enter the host to initiate the Smappee local integration", + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?", + "title": "Discovered Smappee device" + }, "pick_implementation": { "title": "Pick Authentication Method" } + }, + "abort": { + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.", + "authorize_url_timeout": "Timeout generating authorize url.", + "connection_error": "Failed to connect to Smappee device.", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "invalid_mdns": "Unsupported device for the Smappee integration." } } -} \ No newline at end of file +} diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index a61444a42c0..58da782a75f 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -59,6 +59,9 @@ ZEROCONF = { "_spotify-connect._tcp.local.": [ "spotify" ], + "_ssh._tcp.local.": [ + "smappee" + ], "_viziocast._tcp.local.": [ "vizio" ], diff --git a/requirements_all.txt b/requirements_all.txt index 8549dddf313..61d276e060e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1623,7 +1623,7 @@ pyskyqhub==0.1.1 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.1.5 +pysmappee==0.2.9 # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6305c3ae1e4..f1e08cfd658 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -752,7 +752,7 @@ pysignalclirestapi==0.3.4 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.1.5 +pysmappee==0.2.9 # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 265cfde69bb..1d9bb547f61 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -1,16 +1,326 @@ -"""Test the Smappee config flow.""" -from homeassistant import config_entries, setup -from homeassistant.components.smappee.const import AUTHORIZE_URL, DOMAIN, TOKEN_URL +"""Test the Smappee component config flow module.""" +from homeassistant import data_entry_flow, setup +from homeassistant.components.smappee.const import ( + CONF_HOSTNAME, + CONF_SERIALNUMBER, + DOMAIN, + ENV_CLOUD, + ENV_LOCAL, + TOKEN_URL, +) +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF 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_full_flow(hass, aiohttp_client, aioclient_mock): +async def test_show_user_form(hass): + """Test that the user set up form is served.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + + assert result["step_id"] == "environment" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_user_host_form(hass): + """Test that the host form is served after choosing the local option.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + assert result["step_id"] == "environment" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"environment": ENV_LOCAL} + ) + + assert result["step_id"] == ENV_LOCAL + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_zeroconf_connection_error_form(hass): + """Test that the zeroconf confirmation form is served.""" + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "1.2.3.4", + "port": 22, + CONF_HOSTNAME: "Smappee1006000212.local.", + "type": "_ssh._tcp.local.", + "name": "Smappee1006000212._ssh._tcp.local.", + "properties": {"_raw": {}}, + }, + ) + + assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zeroconf_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + +async def test_connection_error(hass): + """Test we show user form on Smappee connection error.""" + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + assert result["step_id"] == "environment" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"environment": ENV_LOCAL} + ) + assert result["step_id"] == ENV_LOCAL + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"} + ) + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_wrong_mdns(hass): + """Test we abort if unsupported mDNS name is discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "1.2.3.4", + "port": 22, + CONF_HOSTNAME: "example.local.", + "type": "_ssh._tcp.local.", + "name": "example._ssh._tcp.local.", + "properties": {"_raw": {}}, + }, + ) + + assert result["reason"] == "invalid_mdns" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_full_user_wrong_mdns(hass): + """Test we abort user flow if unsupported mDNS name got resolved.""" + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee2006000212"}], + ), patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + assert result["step_id"] == "environment" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"environment": ENV_LOCAL} + ) + assert result["step_id"] == ENV_LOCAL + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "invalid_mdns" + + +async def test_user_device_exists_abort(hass): + """Test we abort user flow if Smappee device already configured.""" + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ): + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4"}, + unique_id="1006000212", + source=SOURCE_USER, + ) + config_entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + assert result["step_id"] == "environment" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"environment": ENV_LOCAL} + ) + assert result["step_id"] == ENV_LOCAL + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_zeroconf_device_exists_abort(hass): + """Test we abort zeroconf flow if Smappee device already configured.""" + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ): + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4"}, + unique_id="1006000212", + source=SOURCE_USER, + ) + config_entry.add_to_hass(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "1.2.3.4", + "port": 22, + CONF_HOSTNAME: "Smappee1006000212.local.", + "type": "_ssh._tcp.local.", + "name": "Smappee1006000212._ssh._tcp.local.", + "properties": {"_raw": {}}, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_cloud_device_exists_abort(hass): + """Test we abort cloud flow if Smappee Cloud device already configured.""" + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="smappeeCloud", source=SOURCE_USER, + ) + config_entry.add_to_hass(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured_device" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_zeroconf_abort_if_cloud_device_exists(hass): + """Test we abort zeroconf flow if Smappee Cloud device already configured.""" + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="smappeeCloud", source=SOURCE_USER, + ) + config_entry.add_to_hass(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "1.2.3.4", + "port": 22, + CONF_HOSTNAME: "Smappee1006000212.local.", + "type": "_ssh._tcp.local.", + "name": "Smappee1006000212._ssh._tcp.local.", + "properties": {"_raw": {}}, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured_device" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_zeroconf_confirm_abort_if_cloud_device_exists(hass): + """Test we abort zeroconf confirm flow if Smappee Cloud device already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "1.2.3.4", + "port": 22, + CONF_HOSTNAME: "Smappee1006000212.local.", + "type": "_ssh._tcp.local.", + "name": "Smappee1006000212._ssh._tcp.local.", + "properties": {"_raw": {}}, + }, + ) + + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="smappeeCloud", source=SOURCE_USER, + ) + config_entry.add_to_hass(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured_device" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_abort_cloud_flow_if_local_device_exists(hass): + """Test we abort the cloud flow if a Smappee local device already configured.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4"}, + unique_id="1006000212", + source=SOURCE_USER, + ) + config_entry.add_to_hass(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"environment": ENV_CLOUD} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured_local_device" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_full_user_flow(hass, aiohttp_client, aioclient_mock): """Check full flow.""" assert await setup.async_setup_component( hass, @@ -22,16 +332,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"environment": ENV_CLOUD} ) state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) - assert result["url"] == ( - f"{AUTHORIZE_URL['PRODUCTION']}?response_type=code&client_id={CLIENT_ID}" - "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}" - ) - client = await aiohttp_client(hass.http.app) resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") assert resp.status == 200 @@ -54,3 +361,81 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 + + +async def test_full_zeroconf_flow(hass): + """Test the full zeroconf flow.""" + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ), patch( + "homeassistant.components.smappee.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "1.2.3.4", + "port": 22, + CONF_HOSTNAME: "Smappee1006000212.local.", + "type": "_ssh._tcp.local.", + "name": "Smappee1006000212._ssh._tcp.local.", + "properties": {"_raw": {}}, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zeroconf_confirm" + assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "smappee1006000212" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.unique_id == "1006000212" + + +async def test_full_user_local_flow(hass): + """Test the full zeroconf flow.""" + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ), patch( + "homeassistant.components.smappee.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, + ) + assert result["step_id"] == "environment" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"environment": ENV_LOCAL}, + ) + assert result["step_id"] == ENV_LOCAL + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "smappee1006000212" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.unique_id == "1006000212" diff --git a/tests/components/smappee/test_init.py b/tests/components/smappee/test_init.py new file mode 100644 index 00000000000..9a81441e8b3 --- /dev/null +++ b/tests/components/smappee/test_init.py @@ -0,0 +1,32 @@ +"""Tests for the Smappee component init module.""" +from homeassistant.components.smappee.const import DOMAIN +from homeassistant.config_entries import SOURCE_ZEROCONF + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_unload_config_entry(hass): + """Test unload config entry flow.""" + with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( + "pysmappee.api.SmappeeLocalApi.load_advanced_config", + return_value=[{"key": "mdnsHostName", "value": "Smappee1006000212"}], + ), patch( + "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] + ), patch( + "pysmappee.api.SmappeeLocalApi.load_instantaneous", + return_value=[{"key": "phase0ActivePower", "value": 0}], + ): + config_entry = MockConfigEntry( + domain=DOMAIN, + data={"host": "1.2.3.4"}, + unique_id="smappee1006000212", + source=SOURCE_ZEROCONF, + ) + config_entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) From f1fd8aa51faac0181a6ed1995489f344526e970b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 10 Aug 2020 08:19:38 -0400 Subject: [PATCH 080/862] Add support for Flo by Moen water shutoff devices (#38171) --- CODEOWNERS | 1 + homeassistant/components/flo/__init__.py | 76 +++++ homeassistant/components/flo/config_flow.py | 67 ++++ homeassistant/components/flo/const.py | 3 + homeassistant/components/flo/device.py | 156 +++++++++ homeassistant/components/flo/entity.py | 70 ++++ homeassistant/components/flo/manifest.json | 12 + homeassistant/components/flo/sensor.py | 158 +++++++++ homeassistant/components/flo/strings.json | 22 ++ .../components/flo/translations/en.json | 22 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/flo/__init__.py | 1 + tests/components/flo/common.py | 12 + tests/components/flo/conftest.py | 83 +++++ tests/components/flo/test_config_flow.py | 46 +++ tests/components/flo/test_device.py | 50 +++ tests/components/flo/test_init.py | 16 + tests/components/flo/test_sensor.py | 24 ++ tests/fixtures/flo/device_info_response.json | 238 ++++++++++++++ .../flo/location_info_base_response.json | 89 +++++ ...location_info_expand_devices_response.json | 308 ++++++++++++++++++ .../fixtures/flo/user_info_base_response.json | 34 ++ .../user_info_expand_locations_response.json | 120 +++++++ .../flo/water_consumption_info_response.json | 34 ++ tests/test_util/aiohttp.py | 2 +- 27 files changed, 1650 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/flo/__init__.py create mode 100644 homeassistant/components/flo/config_flow.py create mode 100644 homeassistant/components/flo/const.py create mode 100644 homeassistant/components/flo/device.py create mode 100644 homeassistant/components/flo/entity.py create mode 100644 homeassistant/components/flo/manifest.json create mode 100644 homeassistant/components/flo/sensor.py create mode 100644 homeassistant/components/flo/strings.json create mode 100644 homeassistant/components/flo/translations/en.json create mode 100644 tests/components/flo/__init__.py create mode 100644 tests/components/flo/common.py create mode 100644 tests/components/flo/conftest.py create mode 100644 tests/components/flo/test_config_flow.py create mode 100644 tests/components/flo/test_device.py create mode 100644 tests/components/flo/test_init.py create mode 100644 tests/components/flo/test_sensor.py create mode 100644 tests/fixtures/flo/device_info_response.json create mode 100644 tests/fixtures/flo/location_info_base_response.json create mode 100644 tests/fixtures/flo/location_info_expand_devices_response.json create mode 100644 tests/fixtures/flo/user_info_base_response.json create mode 100644 tests/fixtures/flo/user_info_expand_locations_response.json create mode 100644 tests/fixtures/flo/water_consumption_info_response.json diff --git a/CODEOWNERS b/CODEOWNERS index 3a8bfaa4842..c2198123382 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -134,6 +134,7 @@ homeassistant/components/filter/* @dgomes homeassistant/components/firmata/* @DaAwesomeP homeassistant/components/fixer/* @fabaff homeassistant/components/flick_electric/* @ZephireNZ +homeassistant/components/flo/* @dmulcahey homeassistant/components/flock/* @fabaff homeassistant/components/flume/* @ChrisMandich @bdraco homeassistant/components/flunearyou/* @bachya diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py new file mode 100644 index 00000000000..2c267addb0c --- /dev/null +++ b/homeassistant/components/flo/__init__.py @@ -0,0 +1,76 @@ +"""The flo integration.""" +import asyncio +import logging + +from aioflo import async_get_api +from aioflo.errors import RequestError +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN +from .device import FloDeviceDataUpdateCoordinator + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the flo component.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up flo from a config entry.""" + hass.data[DOMAIN][entry.entry_id] = {} + session = async_get_clientsession(hass) + try: + hass.data[DOMAIN][entry.entry_id]["client"] = client = await async_get_api( + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session + ) + except RequestError: + raise ConfigEntryNotReady + + user_info = await client.user.get_info(include_location_info=True) + + _LOGGER.debug("Flo user information with locations: %s", user_info) + + hass.data[DOMAIN]["devices"] = devices = [ + FloDeviceDataUpdateCoordinator(hass, client, location["id"], device["id"]) + for location in user_info["locations"] + for device in location["devices"] + ] + + tasks = [device.async_refresh() for device in devices] + await asyncio.gather(*tasks) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py new file mode 100644 index 00000000000..1f8e5fc08bd --- /dev/null +++ b/homeassistant/components/flo/config_flow.py @@ -0,0 +1,67 @@ +"""Config flow for flo integration.""" +import logging + +from aioflo import async_get_api +from aioflo.errors import RequestError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({"username": str, "password": str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + session = async_get_clientsession(hass) + try: + api = await async_get_api( + data[CONF_USERNAME], data[CONF_PASSWORD], session=session + ) + except RequestError: + raise CannotConnect + except Exception: # pylint: disable=broad-except + raise CannotConnect + + user_info = await api.user.get_info() + a_location_id = user_info["locations"][0]["id"] + location_info = await api.location.get_info(a_location_id) + return {"title": location_info["nickname"]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for flo.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/flo/const.py b/homeassistant/components/flo/const.py new file mode 100644 index 00000000000..edeb469380b --- /dev/null +++ b/homeassistant/components/flo/const.py @@ -0,0 +1,3 @@ +"""Constants for the flo integration.""" + +DOMAIN = "flo" diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py new file mode 100644 index 00000000000..ad7973f4c9a --- /dev/null +++ b/homeassistant/components/flo/device.py @@ -0,0 +1,156 @@ +"""Flo device object.""" +import asyncio +from datetime import datetime, timedelta +import logging +from typing import Any, Dict, Optional + +from aioflo.api import API +from aioflo.errors import RequestError +from async_timeout import timeout + +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import homeassistant.util.dt as dt_util + +from .const import DOMAIN as FLO_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): + """Flo device object.""" + + def __init__( + self, hass: HomeAssistantType, api_client: API, location_id: str, device_id: str + ): + """Initialize the device.""" + self.hass: HomeAssistantType = hass + self.api_client: API = api_client + self._flo_location_id: str = location_id + self._flo_device_id: str = device_id + self._manufacturer: str = "Flo by Moen" + self._device_information: Optional[Dict[str, Any]] = None + self._water_usage: Optional[Dict[str, Any]] = None + super().__init__( + hass, + _LOGGER, + name=f"{FLO_DOMAIN}-{device_id}", + update_interval=timedelta(seconds=60), + ) + + async def _async_update_data(self): + """Update data via library.""" + try: + async with timeout(10): + await asyncio.gather( + *[self._update_device(), self._update_consumption_data()] + ) + except (RequestError) as error: + raise UpdateFailed(error) + + @property + def location_id(self) -> str: + """Return Flo location id.""" + return self._flo_location_id + + @property + def id(self) -> str: + """Return Flo device id.""" + return self._flo_device_id + + @property + def device_name(self) -> str: + """Return device name.""" + return f"{self.manufacturer} {self.model}" + + @property + def manufacturer(self) -> str: + """Return manufacturer for device.""" + return self._manufacturer + + @property + def mac_address(self) -> str: + """Return ieee address for device.""" + return self._device_information["macAddress"] + + @property + def model(self) -> str: + """Return model for device.""" + return self._device_information["deviceModel"] + + @property + def rssi(self) -> float: + """Return rssi for device.""" + return self._device_information["connectivity"]["rssi"] + + @property + def last_heard_from_time(self) -> str: + """Return lastHeardFromTime for device.""" + return self._device_information["lastHeardFromTime"] + + @property + def device_type(self) -> str: + """Return the device type for the device.""" + return self._device_information["deviceType"] + + @property + def available(self) -> bool: + """Return True if device is available.""" + return self.last_update_success and self._device_information["isConnected"] + + @property + def current_system_mode(self) -> str: + """Return the current system mode.""" + return self._device_information["systemMode"]["lastKnown"] + + @property + def target_system_mode(self) -> str: + """Return the target system mode.""" + return self._device_information["systemMode"]["target"] + + @property + def current_flow_rate(self) -> float: + """Return current flow rate in gpm.""" + return self._device_information["telemetry"]["current"]["gpm"] + + @property + def current_psi(self) -> float: + """Return the current pressure in psi.""" + return self._device_information["telemetry"]["current"]["psi"] + + @property + def temperature(self) -> float: + """Return the current temperature in degrees F.""" + return self._device_information["telemetry"]["current"]["tempF"] + + @property + def consumption_today(self) -> float: + """Return the current consumption for today in gallons.""" + return self._water_usage["aggregations"]["sumTotalGallonsConsumed"] + + @property + def firmware_version(self) -> str: + """Return the firmware version for the device.""" + return self._device_information["fwVersion"] + + @property + def serial_number(self) -> str: + """Return the serial number for the device.""" + return self._device_information["serialNumber"] + + async def _update_device(self, *_) -> None: + """Update the device information from the API.""" + self._device_information = await self.api_client.device.get_info( + self._flo_device_id + ) + _LOGGER.debug("Flo device data: %s", self._device_information) + + async def _update_consumption_data(self, *_) -> None: + """Update water consumption data from the API.""" + today = dt_util.now().date() + start_date = datetime(today.year, today.month, today.day, 0, 0) + end_date = datetime(today.year, today.month, today.day, 23, 59, 59, 999000) + self._water_usage = await self.api_client.water.get_consumption_info( + self._flo_location_id, start_date, end_date + ) + _LOGGER.debug("Updated Flo consumption data: %s", self._water_usage) diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py new file mode 100644 index 00000000000..10ffa835454 --- /dev/null +++ b/homeassistant/components/flo/entity.py @@ -0,0 +1,70 @@ +"""Base entity class for Flo entities.""" + +from typing import Any, Dict + +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN as FLO_DOMAIN +from .device import FloDeviceDataUpdateCoordinator + + +class FloEntity(Entity): + """A base class for Flo entities.""" + + def __init__( + self, + entity_type: str, + name: str, + device: FloDeviceDataUpdateCoordinator, + **kwargs, + ): + """Init Flo entity.""" + self._unique_id: str = f"{device.mac_address}_{entity_type}" + self._name: str = name + self._device: FloDeviceDataUpdateCoordinator = device + self._state: Any = None + + @property + def name(self) -> str: + """Return Entity's default name.""" + return self._name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def device_info(self) -> Dict[str, Any]: + """Return a device description for device registry.""" + return { + "identifiers": {(FLO_DOMAIN, self._device.id)}, + "connections": {(CONNECTION_NETWORK_MAC, self._device.mac_address)}, + "manufacturer": self._device.manufacturer, + "model": self._device.model, + "name": self._device.device_name, + "sw_version": self._device.firmware_version, + } + + @property + def available(self) -> bool: + """Return True if device is available.""" + return self._device.available + + @property + def force_update(self) -> bool: + """Force update this entity.""" + return False + + @property + def should_poll(self) -> bool: + """Poll state from device.""" + return True + + async def async_update(self): + """Update Flo entity.""" + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove(self._device.async_add_listener(self.async_write_ha_state)) diff --git a/homeassistant/components/flo/manifest.json b/homeassistant/components/flo/manifest.json new file mode 100644 index 00000000000..cfcb6db1c5f --- /dev/null +++ b/homeassistant/components/flo/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "flo", + "name": "Flo", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/flo", + "requirements": ["aioflo==0.4.0"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@dmulcahey"] +} diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py new file mode 100644 index 00000000000..2cbc43e8cd8 --- /dev/null +++ b/homeassistant/components/flo/sensor.py @@ -0,0 +1,158 @@ +"""Support for Flo Water Monitor sensors.""" + +from typing import List, Optional + +from homeassistant.const import ( + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_PSI, + TEMP_CELSIUS, + VOLUME_GALLONS, +) +from homeassistant.util.temperature import fahrenheit_to_celsius + +from .const import DOMAIN as FLO_DOMAIN +from .device import FloDeviceDataUpdateCoordinator +from .entity import FloEntity + +DEPENDENCIES = ["flo"] + +WATER_ICON = "mdi:water" +GAUGE_ICON = "mdi:gauge" +NAME_DAILY_USAGE = "Today's Water Usage" +NAME_CURRENT_SYSTEM_MODE = "Current System Mode" +NAME_FLOW_RATE = "Water Flow Rate" +NAME_TEMPERATURE = "Water Temperature" +NAME_WATER_PRESSURE = "Water Pressure" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Flo sensors from config entry.""" + devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN]["devices"] + entities = [] + entities.extend([FloDailyUsageSensor(device) for device in devices]) + entities.extend([FloSystemModeSensor(device) for device in devices]) + entities.extend([FloCurrentFlowRateSensor(device) for device in devices]) + entities.extend([FloTemperatureSensor(device) for device in devices]) + entities.extend([FloPressureSensor(device) for device in devices]) + async_add_entities(entities, True) + + +class FloDailyUsageSensor(FloEntity): + """Monitors the daily water usage.""" + + def __init__(self, device): + """Initialize the daily water usage sensor.""" + super().__init__("daily_consumption", NAME_DAILY_USAGE, device) + self._state: float = None + + @property + def icon(self) -> str: + """Return the daily usage icon.""" + return WATER_ICON + + @property + def state(self) -> Optional[float]: + """Return the current daily usage.""" + if self._device.consumption_today is None: + return None + return round(self._device.consumption_today, 1) + + @property + def unit_of_measurement(self) -> str: + """Return gallons as the unit measurement for water.""" + return VOLUME_GALLONS + + +class FloSystemModeSensor(FloEntity): + """Monitors the current Flo system mode.""" + + def __init__(self, device): + """Initialize the system mode sensor.""" + super().__init__("current_system_mode", NAME_CURRENT_SYSTEM_MODE, device) + self._state: str = None + + @property + def state(self) -> Optional[str]: + """Return the current system mode.""" + if not self._device.current_system_mode: + return None + return self._device.current_system_mode + + +class FloCurrentFlowRateSensor(FloEntity): + """Monitors the current water flow rate.""" + + def __init__(self, device): + """Initialize the flow rate sensor.""" + super().__init__("current_flow_rate", NAME_FLOW_RATE, device) + self._state: float = None + + @property + def icon(self) -> str: + """Return the daily usage icon.""" + return GAUGE_ICON + + @property + def state(self) -> Optional[float]: + """Return the current flow rate.""" + if self._device.current_flow_rate is None: + return None + return round(self._device.current_flow_rate, 1) + + @property + def unit_of_measurement(self) -> str: + """Return the unit measurement.""" + return "gpm" + + +class FloTemperatureSensor(FloEntity): + """Monitors the temperature.""" + + def __init__(self, device): + """Initialize the temperature sensor.""" + super().__init__("temperature", NAME_TEMPERATURE, device) + self._state: float = None + + @property + def state(self) -> Optional[float]: + """Return the current temperature.""" + if self._device.temperature is None: + return None + return round(fahrenheit_to_celsius(self._device.temperature), 1) + + @property + def unit_of_measurement(self) -> str: + """Return gallons as the unit measurement for water.""" + return TEMP_CELSIUS + + @property + def device_class(self) -> Optional[str]: + """Return the device class for this sensor.""" + return DEVICE_CLASS_TEMPERATURE + + +class FloPressureSensor(FloEntity): + """Monitors the water pressure.""" + + def __init__(self, device): + """Initialize the pressure sensor.""" + super().__init__("water_pressure", NAME_WATER_PRESSURE, device) + self._state: float = None + + @property + def state(self) -> Optional[float]: + """Return the current water pressure.""" + if self._device.current_psi is None: + return None + return round(self._device.current_psi, 1) + + @property + def unit_of_measurement(self) -> str: + """Return gallons as the unit measurement for water.""" + return PRESSURE_PSI + + @property + def device_class(self) -> Optional[str]: + """Return the device class for this sensor.""" + return DEVICE_CLASS_PRESSURE diff --git a/homeassistant/components/flo/strings.json b/homeassistant/components/flo/strings.json new file mode 100644 index 00000000000..7da0d2df2be --- /dev/null +++ b/homeassistant/components/flo/strings.json @@ -0,0 +1,22 @@ +{ + "title": "flo", + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/en.json b/homeassistant/components/flo/translations/en.json new file mode 100644 index 00000000000..9434a81b3e6 --- /dev/null +++ b/homeassistant/components/flo/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + } + } + }, + "title": "flo" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1bf776f0849..f6171eb9f47 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -51,6 +51,7 @@ FLOWS = [ "enocean", "esphome", "flick_electric", + "flo", "flume", "flunearyou", "forked_daapd", diff --git a/requirements_all.txt b/requirements_all.txt index 61d276e060e..bcd39c115c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,6 +160,9 @@ aiodns==2.0.0 # homeassistant.components.esphome aioesphomeapi==2.6.1 +# homeassistant.components.flo +aioflo==0.4.0 + # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1e08cfd658..3c1a97e6bfd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -88,6 +88,9 @@ aiodns==2.0.0 # homeassistant.components.esphome aioesphomeapi==2.6.1 +# homeassistant.components.flo +aioflo==0.4.0 + # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/tests/components/flo/__init__.py b/tests/components/flo/__init__.py new file mode 100644 index 00000000000..a207193f500 --- /dev/null +++ b/tests/components/flo/__init__.py @@ -0,0 +1 @@ +"""Tests for the flo integration.""" diff --git a/tests/components/flo/common.py b/tests/components/flo/common.py new file mode 100644 index 00000000000..d4018aae090 --- /dev/null +++ b/tests/components/flo/common.py @@ -0,0 +1,12 @@ +"""Define common test utilities.""" +TEST_ACCOUNT_ID = "aabbccdd" +TEST_DEVICE_ID = "98765" +TEST_EMAIL_ADDRESS = "email@address.com" +TEST_FIRST_NAME = "Tom" +TEST_LAST_NAME = "Jones" +TEST_LOCATION_ID = "mmnnoopp" +TEST_MAC_ADDRESS = "12:34:56:ab:cd:ef" +TEST_PASSWORD = "password" +TEST_PHONE_NUMBER = "+1 123-456-7890" +TEST_TOKEN = "123abc" +TEST_USER_ID = "12345abcde" diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py new file mode 100644 index 00000000000..5790d3d4eb3 --- /dev/null +++ b/tests/components/flo/conftest.py @@ -0,0 +1,83 @@ +"""Define fixtures available for all tests.""" +import json +import time + +import pytest + +from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def config_entry(hass): + """Config entry version 1 fixture.""" + return MockConfigEntry( + domain=FLO_DOMAIN, + data={CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD}, + version=1, + ) + + +@pytest.fixture +def aioclient_mock_fixture(aioclient_mock): + """Fixture to provide a aioclient mocker.""" + + now = round(time.time()) + + # Mocks the login response for flo. + aioclient_mock.post( + "https://api.meetflo.com/api/v1/users/auth", + text=json.dumps( + { + "token": TEST_TOKEN, + "tokenPayload": { + "user": {"user_id": TEST_USER_ID, "email": TEST_EMAIL_ADDRESS}, + "timestamp": now, + }, + "tokenExpiration": 86400, + "timeNow": now, + } + ), + headers={"Content-Type": "application/json"}, + status=200, + ) + # Mocks the device for flo. + aioclient_mock.get( + "https://api-gw.meetflo.com/api/v2/devices/98765", + text=load_fixture("flo/device_info_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + ) + # Mocks the water consumption for flo. + aioclient_mock.get( + "https://api-gw.meetflo.com/api/v2/water/consumption", + text=load_fixture("flo/water_consumption_info_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + ) + # Mocks the location info for flo. + aioclient_mock.get( + "https://api-gw.meetflo.com/api/v2/locations/mmnnoopp", + text=load_fixture("flo/location_info_expand_devices_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + ) + # Mocks the user info for flo. + aioclient_mock.get( + "https://api-gw.meetflo.com/api/v2/users/12345abcde", + text=load_fixture("flo/user_info_expand_locations_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + params={"expand": "locations"}, + ) + # Mocks the user info for flo. + aioclient_mock.get( + "https://api-gw.meetflo.com/api/v2/users/12345abcde", + text=load_fixture("flo/user_info_expand_locations_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + ) diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py new file mode 100644 index 00000000000..bddea76e73c --- /dev/null +++ b/tests/components/flo/test_config_flow.py @@ -0,0 +1,46 @@ +"""Test the flo config flow.""" +from homeassistant import config_entries, setup +from homeassistant.components.flo.const import DOMAIN + +from tests.async_mock import patch + + +async def test_form(hass, aioclient_mock_fixture): + """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.flo.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.flo.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Home" + assert result2["data"] == {"username": "test-username", "password": "test-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, aioclient_mock): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"username": "test-username", "password": "test-password"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py new file mode 100644 index 00000000000..13f6cd5293a --- /dev/null +++ b/tests/components/flo/test_device.py @@ -0,0 +1,50 @@ +"""Define tests for device-related endpoints.""" +from datetime import timedelta + +from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN +from homeassistant.components.flo.device import FloDeviceDataUpdateCoordinator +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from .common import TEST_PASSWORD, TEST_USER_ID + +from tests.common import async_fire_time_changed + + +async def test_device(hass, config_entry, aioclient_mock_fixture, aioclient_mock): + """Test Flo by Moen device.""" + config_entry.add_to_hass(hass) + assert await async_setup_component( + hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} + ) + await hass.async_block_till_done() + assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + + device: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN]["devices"][0] + assert device.api_client is not None + assert device.available + assert device.consumption_today == 3.674 + assert device.current_flow_rate == 0 + assert device.current_psi == 54.20000076293945 + assert device.current_system_mode == "home" + assert device.target_system_mode == "home" + assert device.firmware_version == "6.1.1" + assert device.device_type == "flo_device_v2" + assert device.id == "98765" + assert device.last_heard_from_time == "2020-07-24T12:45:00Z" + assert device.location_id == "mmnnoopp" + assert device.hass is not None + assert device.temperature == 70 + assert device.mac_address == "111111111111" + assert device.model == "flo_device_075_v2" + assert device.manufacturer == "Flo by Moen" + assert device.device_name == "Flo by Moen flo_device_075_v2" + assert device.rssi == -47 + + call_count = aioclient_mock.call_count + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=90)) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == call_count + 2 diff --git a/tests/components/flo/test_init.py b/tests/components/flo/test_init.py new file mode 100644 index 00000000000..c0eaf535f35 --- /dev/null +++ b/tests/components/flo/test_init.py @@ -0,0 +1,16 @@ +"""Test init.""" +from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from .common import TEST_PASSWORD, TEST_USER_ID + + +async def test_setup_entry(hass, config_entry, aioclient_mock_fixture): + """Test migration of config entry from v1.""" + config_entry.add_to_hass(hass) + assert await async_setup_component( + hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} + ) + await hass.async_block_till_done() + assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py new file mode 100644 index 00000000000..5db1fdacfe1 --- /dev/null +++ b/tests/components/flo/test_sensor.py @@ -0,0 +1,24 @@ +"""Test Flo by Moen sensor entities.""" +from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from .common import TEST_PASSWORD, TEST_USER_ID + + +async def test_sensors(hass, config_entry, aioclient_mock_fixture): + """Test Flo by Moen sensors.""" + config_entry.add_to_hass(hass) + assert await async_setup_component( + hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} + ) + await hass.async_block_till_done() + + assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + + # we should have 5 entities for the device + assert hass.states.get("sensor.current_system_mode").state == "home" + assert hass.states.get("sensor.today_s_water_usage").state == "3.7" + assert hass.states.get("sensor.water_flow_rate").state == "0" + assert hass.states.get("sensor.water_pressure").state == "54.2" + assert hass.states.get("sensor.water_temperature").state == "21.1" diff --git a/tests/fixtures/flo/device_info_response.json b/tests/fixtures/flo/device_info_response.json new file mode 100644 index 00000000000..24351c0e632 --- /dev/null +++ b/tests/fixtures/flo/device_info_response.json @@ -0,0 +1,238 @@ +{ + "isConnected": true, + "fwVersion": "6.1.1", + "lastHeardFromTime": "2020-07-24T12:45:00Z", + "fwProperties": { + "alarm_away_high_flow_rate_shut_off_enabled": true, + "alarm_away_high_water_use_shut_off_enabled": true, + "alarm_away_long_flow_event_shut_off_enabled": true, + "alarm_away_v2_shut_off_enabled": true, + "alarm_home_high_flow_rate_shut_off_deferment": 300, + "alarm_home_high_flow_rate_shut_off_enabled": true, + "alarm_home_high_water_use_shut_off_deferment": 300, + "alarm_home_high_water_use_shut_off_enabled": true, + "alarm_home_long_flow_event_shut_off_deferment": 300, + "alarm_home_long_flow_event_shut_off_enabled": true, + "alarm_shut_off_enabled": true, + "alarm_shutoff_id": "", + "alarm_shutoff_time_epoch_sec": -1, + "alarm_snooze_enabled": true, + "alarm_suppress_duplicate_duration": 300, + "alarm_suppress_until_event_end": false, + "data_flosense_force_retrain": 1, + "data_flosense_min_flodetect_sec": 0, + "data_flosense_min_irr_sec": 180, + "data_flosense_status_interval": 1200, + "data_flosense_verbosity": 1, + "device_data_free_mb": 1465, + "device_installed": true, + "device_mem_available_kb": 339456, + "device_rootfs_free_kb": 711504, + "device_uptime_sec": 867190, + "feature_mode": "default", + "flodetect_post_enabled": true, + "flodetect_post_frequency": 0, + "flodetect_storage_days": 60, + "flosense_action": "", + "flosense_deployment_result": "success", + "flosense_link": "", + "flosense_shut_off_enabled": true, + "flosense_shut_off_level": 3, + "flosense_state": "active", + "flosense_version_app": "2.5.3", + "flosense_version_model": "2.5.0", + "fw_ver": "6.1.1", + "fw_ver_a": "6.1.1", + "fw_ver_b": "6.0.3", + "heartbeat_frequency": 1800, + "ht_attempt_interval": 60000, + "ht_check_window_max_pressure_decay_limit": 0.1, + "ht_check_window_width": 30000, + "ht_controller": "ultima", + "ht_max_open_closed_pressure_decay_pct_limit": 2, + "ht_max_pressure_growth_limit": 3, + "ht_max_pressure_growth_pct_limit": 3, + "ht_max_valve_closures_per_24h": 0, + "ht_min_computable_point_limit": 3, + "ht_min_pressure_limit": 10, + "ht_min_r_squared_limit": 0.9, + "ht_min_slope_limit": -0.6, + "ht_phase_1_max_pressure_decay_limit": 6, + "ht_phase_1_max_pressure_decay_pct_limit": 10, + "ht_phase_1_time_index": 12000, + "ht_phase_2_max_pressure_decay_limit": 6, + "ht_phase_2_max_pressure_decay_pct_limit": 10, + "ht_phase_2_time_index": 30000, + "ht_phase_3_max_pressure_decay_limit": 3, + "ht_phase_3_max_pressure_decay_pct_limit": 5, + "ht_phase_3_time_index": 240000, + "ht_phase_4_max_pressure_decay_limit": 1.5, + "ht_phase_4_max_pressure_decay_pct_limit": 5, + "ht_phase_4_time_index": 480000, + "ht_pre_delay": 0, + "ht_recent_flow_event_cool_down": 1000, + "ht_retry_on_fail_interval": 900000, + "ht_scheduler": "flosense", + "ht_scheduler_end": "08:00", + "ht_scheduler_start": "06:00", + "ht_scheduler_ultima_allotted_time_1": "06:00", + "ht_scheduler_ultima_allotted_time_2": "07:00", + "ht_scheduler_ultima_allotted_time_3": "", + "ht_times_per_day": 1, + "log_bytes_sent": 0, + "log_enabled": true, + "log_frequency": 3600, + "log_send": false, + "mender_check": false, + "mender_host": "https://mender.flotech.co", + "mender_parts_link": "", + "mender_ping_delay": 300, + "mender_signature": "20200610", + "motor_delay_close": 175, + "motor_delay_open": 0, + "motor_retry_count": 2, + "motor_timeout": 5000, + "mqtt_host": "mqtt.flosecurecloud.com", + "mqtt_port": 8884, + "pes_away_max_duration": 1505, + "pes_away_max_pressure": 150, + "pes_away_max_temperature": 226, + "pes_away_max_volume": 91.8913240498193, + "pes_away_min_pressure": 20, + "pes_away_min_pressure_duration": 5, + "pes_away_min_temperature": 36, + "pes_away_min_temperature_duration": 10, + "pes_away_v1_high_flow_rate": 7.825131772346, + "pes_away_v1_high_flow_rate_duration": 5, + "pes_away_v2_high_flow_rate": 0.5, + "pes_away_v2_high_flow_rate_duration": 5, + "pes_home_high_flow_rate": 1000, + "pes_home_high_flow_rate_duration": 20, + "pes_home_max_duration": 7431, + "pes_home_max_pressure": 150, + "pes_home_max_temperature": 226, + "pes_home_max_volume": 185.56459045410156, + "pes_home_min_pressure": 20, + "pes_home_min_pressure_duration": 5, + "pes_home_min_temperature": 36, + "pes_home_min_temperature_duration": 10, + "pes_moderately_high_pressure": 80, + "pes_moderately_high_pressure_count": 43200, + "pes_moderately_high_pressure_delay": 300, + "pes_moderately_high_pressure_period": 10, + "player_action": "disabled", + "player_flow": 0, + "player_min_pressure": 40, + "player_pressure": 60, + "player_temperature": 50, + "power_downtime_last_24h": 91, + "power_downtime_last_7days": 91, + "power_downtime_last_reboot": 91, + "pt_state": "ok", + "reboot_count": 26, + "reboot_count_7days": 1, + "reboot_reason": "power_cycle", + "s3_bucket_host": "api-bulk.meetflo.com", + "serial_number": "111111111111", + "system_mode": 2, + "tag": "", + "telemetry_batched_enabled": true, + "telemetry_batched_hf_enabled": true, + "telemetry_batched_hf_interval": 10800, + "telemetry_batched_hf_poll_rate": 100, + "telemetry_batched_interval": 300, + "telemetry_batched_pending_storage": 30, + "telemetry_batched_sent_storage": 30, + "telemetry_flow_rate": 0, + "telemetry_pressure": 42.4, + "telemetry_realtime_change_gpm": 0, + "telemetry_realtime_change_psi": 0, + "telemetry_realtime_enabled": true, + "telemetry_realtime_interval": 1, + "telemetry_realtime_packet_uptime": 0, + "telemetry_realtime_session_last_epoch": 1595555701518, + "telemetry_realtime_sessions_7days": 25, + "telemetry_realtime_storage": 7, + "telemetry_realtime_timeout": 300, + "telemetry_temperature": 68, + "valve_actuation_count": 906, + "valve_actuation_timeout_count": 0, + "valve_state": 1, + "vpn_enabled": false, + "vpn_ip": "", + "water_event_enabled": false, + "water_event_min_duration": 2, + "water_event_min_gallons": 0.1, + "wifi_bytes_received": 24164, + "wifi_bytes_sent": 18319, + "wifi_disconnections": 76, + "wifi_rssi": -50, + "wifi_sta_enc": "psk2", + "wifi_sta_ip": "192.168.1.1", + "wifi_sta_ssid": "SOMESSID", + "zit_auto_count": 2363, + "zit_manual_count": 0 + }, + "id": "98765", + "macAddress": "111111111111", + "nickname": "Smart Water Shutoff", + "isPaired": true, + "deviceModel": "flo_device_075_v2", + "deviceType": "flo_device_v2", + "irrigationType": "sprinklers", + "systemMode": { + "isLocked": false, + "shouldInherit": true, + "lastKnown": "home", + "target": "home" + }, + "valve": { "target": "open", "lastKnown": "open" }, + "installStatus": { + "isInstalled": true, + "installDate": "2019-05-04T13:50:04.758Z" + }, + "learning": { "outOfLearningDate": "2019-05-10T21:45:48.916Z" }, + "notifications": { + "pending": { + "infoCount": 0, + "warningCount": 2, + "criticalCount": 0, + "alarmCount": [ + { "id": 30, "severity": "warning", "count": 1 }, + { "id": 31, "severity": "warning", "count": 1 } + ], + "info": { "count": 0, "devices": { "count": 0, "absolute": 0 } }, + "warning": { "count": 2, "devices": { "count": 1, "absolute": 1 } }, + "critical": { "count": 0, "devices": { "count": 0, "absolute": 0 } } + } + }, + "hardwareThresholds": { + "gpm": { "okMin": 0, "okMax": 29, "minValue": 0, "maxValue": 35 }, + "psi": { "okMin": 30, "okMax": 80, "minValue": 0, "maxValue": 100 }, + "lpm": { "okMin": 0, "okMax": 110, "minValue": 0, "maxValue": 130 }, + "kPa": { "okMin": 210, "okMax": 550, "minValue": 0, "maxValue": 700 }, + "tempF": { "okMin": 50, "okMax": 80, "minValue": 0, "maxValue": 100 }, + "tempC": { "okMin": 10, "okMax": 30, "minValue": 0, "maxValue": 40 } + }, + "serialNumber": "111111111111", + "connectivity": { "rssi": -47, "ssid": "SOMESSID" }, + "telemetry": { + "current": { + "gpm": 0, + "psi": 54.20000076293945, + "tempF": 70, + "updated": "2020-07-24T12:20:58Z" + } + }, + "healthTest": { + "config": { + "enabled": true, + "timesPerDay": 1, + "start": "02:00", + "end": "04:00" + } + }, + "shutoff": { "scheduledAt": "1970-01-01T00:00:00.000Z" }, + "actionRules": [], + "location": { "id": "mmnnoopp" } +} diff --git a/tests/fixtures/flo/location_info_base_response.json b/tests/fixtures/flo/location_info_base_response.json new file mode 100644 index 00000000000..f6840a0742b --- /dev/null +++ b/tests/fixtures/flo/location_info_base_response.json @@ -0,0 +1,89 @@ +{ + "id": "mmnnoopp", + "users": [ + { + "id": "12345abcde" + } + ], + "devices": [ + { + "id": "98765", + "macAddress": "123456abcdef" + } + ], + "userRoles": [ + { + "userId": "12345abcde", + "roles": [ + "owner" + ] + } + ], + "address": "123 Main Street", + "city": "Boston", + "state": "MA", + "country": "us", + "postalCode": "12345", + "timezone": "US/Easter", + "gallonsPerDayGoal": 240, + "occupants": 2, + "stories": 2, + "isProfileComplete": true, + "nickname": "Home", + "irrigationSchedule": { + "isEnabled": false + }, + "systemMode": { + "target": "home" + }, + "locationType": "sfh", + "locationSize": "lte_4000_sq_ft", + "waterShutoffKnown": "unsure", + "indoorAmenities": [], + "outdoorAmenities": [], + "plumbingAppliances": [ + "exp_tank" + ], + "notifications": { + "pending": { + "infoCount": 0, + "warningCount": 1, + "criticalCount": 0, + "alarmCount": [ + { + "id": 57, + "severity": "warning", + "count": 1 + } + ] + } + }, + "areas": { + "default": [ + { + "id": "xxxxx", + "name": "Attic" + }, + { + "id": "xxxxx", + "name": "Basement" + }, + { + "id": "xxxxx", + "name": "Garage" + }, + { + "id": "xxxxx", + "name": "Main Floor" + }, + { + "id": "xxxxx", + "name": "Upstairs" + } + ], + "custom": [] + }, + "account": { + "id": "aabbccdd" + } +} diff --git a/tests/fixtures/flo/location_info_expand_devices_response.json b/tests/fixtures/flo/location_info_expand_devices_response.json new file mode 100644 index 00000000000..138de88db25 --- /dev/null +++ b/tests/fixtures/flo/location_info_expand_devices_response.json @@ -0,0 +1,308 @@ +{ + "id": "mmnnoopp", + "users": [ + { + "id": "12345abcde" + } + ], + "devices": [ + { + "isConnected": true, + "fwVersion": "4.2.4", + "lastHeardFromTime": "2020-01-16T19:42:06Z", + "fwProperties": { + "alarm_home_high_flow_rate_shut_off_deferment": 300, + "alarm_home_high_water_use_shut_off_deferment": 300, + "alarm_home_long_flow_event_shut_off_deferment": 300, + "alarm_shutoff_time_epoch_sec": -1, + "alarm_snooze_enabled": true, + "alarm_suppress_duplicate_duration": 300, + "alarm_suppress_until_event_end": false, + "data_flosense_force_retrain": 0, + "data_flosense_status_interval": 1200, + "data_flosense_verbosity": 1, + "device_data_free_mb": 1464, + "device_installed": true, + "device_mem_available_kb": 292780, + "device_rootfs_free_kb": 802604, + "device_uptime_sec": 334862, + "flosense_action": "start", + "flosense_deployment_result": "success", + "flosense_link": "", + "flosense_shut_off_enabled": true, + "flosense_shut_off_level": 2, + "flosense_state": "active", + "flosense_version_app": "2.0.0", + "flosense_version_model": "2.0.0", + "fw_ver": "4.2.4", + "fw_ver_a": "4.1.5", + "fw_ver_b": "4.2.4", + "ht_attempt_interval": 60000, + "ht_check_window_max_pressure_decay_limit": 0.1, + "ht_check_window_width": 30000, + "ht_max_open_closed_pressure_decay_pct_limit": 2, + "ht_max_pressure_growth_limit": 3, + "ht_max_pressure_growth_pct_limit": 3, + "ht_min_computable_point_limit": 3, + "ht_min_pressure_limit": 10, + "ht_min_r_squared_limit": 0.9, + "ht_min_slope_limit": -0.6, + "ht_phase_1_max_pressure_decay_limit": 6, + "ht_phase_1_max_pressure_decay_pct_limit": 10, + "ht_phase_1_time_index": 12000, + "ht_phase_2_max_pressure_decay_limit": 6, + "ht_phase_2_max_pressure_decay_pct_limit": 10, + "ht_phase_2_time_index": 30000, + "ht_phase_3_max_pressure_decay_limit": 3, + "ht_phase_3_max_pressure_decay_pct_limit": 5, + "ht_phase_3_time_index": 240000, + "ht_phase_4_max_pressure_decay_limit": 1.5, + "ht_phase_4_max_pressure_decay_pct_limit": 5, + "ht_phase_4_time_index": 480000, + "ht_pre_delay": 0, + "ht_recent_flow_event_cool_down": 1000, + "ht_retry_on_fail_interval": 900000, + "ht_times_per_day": 1, + "log_bytes_sent": 176255, + "log_frequency": 3600, + "mender_host": "https://mender.flotech.co", + "motor_delay_close": 175, + "motor_delay_open": 0, + "motor_retry_count": 2, + "motor_timeout": 5000, + "pes_away_max_duration": 3600, + "pes_away_max_pressure": 150, + "pes_away_max_temperature": 226, + "pes_away_max_volume": 50, + "pes_away_min_pressure": 20, + "pes_away_min_temperature": 36, + "pes_away_v1_high_flow_rate": 8, + "pes_away_v1_high_flow_rate_duration": 5, + "pes_away_v2_high_flow_rate": 0.5, + "pes_away_v2_high_flow_rate_duration": 5, + "pes_home_high_flow_rate": 9.902778339386035, + "pes_home_high_flow_rate_duration": 20, + "pes_home_max_duration": 1738, + "pes_home_max_pressure": 150, + "pes_home_max_temperature": 226, + "pes_home_max_volume": 33.851015281677256, + "pes_home_min_pressure": 20, + "pes_home_min_temperature": 36, + "pes_moderately_high_pressure": 80, + "pes_moderately_high_pressure_count": 43200, + "pes_moderately_high_pressure_delay": 300, + "pes_moderately_high_pressure_period": 10, + "player_action": "disabled", + "player_flow": 0, + "player_min_pressure": 40, + "player_pressure": 60, + "player_temperature": 50, + "power_downtime_last_24h": 0, + "power_downtime_last_7days": 69, + "power_downtime_last_reboot": 0, + "reboot_count": 27, + "reboot_count_7days": 2, + "reboot_reason": "power_cycle", + "s3_bucket_host": "api-bulk.meetflo.com", + "serial_number": "294215640115", + "system_mode": 2, + "telemetry_batched_enabled": true, + "telemetry_batched_interval": 300, + "telemetry_batched_pending_storage": 30, + "telemetry_batched_sent_storage": 30, + "telemetry_flow_rate": 0, + "telemetry_pressure": 78.07500375373304, + "telemetry_realtime_change_gpm": 0, + "telemetry_realtime_change_psi": 0, + "telemetry_realtime_interval": 1, + "telemetry_realtime_session_last_epoch": 0, + "telemetry_realtime_sessions_7days": 0, + "telemetry_realtime_storage": 7, + "telemetry_realtime_timeout": 299, + "telemetry_temperature": 57.00000047232966, + "valve_actuation_count": 3465, + "valve_actuation_timeout_count": 0, + "valve_state": 1, + "wifi_bytes_received": 145018827, + "wifi_bytes_sent": 80891494, + "wifi_disconnections": 423, + "wifi_rssi": -61, + "wifi_sta_enc": "psk2", + "wifi_sta_ssid": "IP freely", + "zit_auto_count": 233, + "zit_manual_count": 0 + }, + "id": "98765", + "macAddress": "123456abcdef", + "nickname": "Smart Water Shutoff", + "isPaired": true, + "deviceModel": "flo_device_075_v2", + "deviceType": "flo_device_v2", + "irrigationType": "sprinklers", + "systemMode": { + "isLocked": false, + "shouldInherit": true, + "lastKnown": "home", + "target": "home" + }, + "valve": { + "target": "open", + "lastKnown": "open" + }, + "installStatus": { + "isInstalled": true, + "installDate": "2018-08-16T02:07:39.483Z" + }, + "learning": { + "outOfLearningDate": "2018-08-16T02:07:39.483Z" + }, + "notifications": { + "pending": { + "infoCount": 0, + "warningCount": 1, + "criticalCount": 0, + "alarmCount": [ + { + "id": 57, + "severity": "warning", + "count": 1 + } + ] + } + }, + "hardwareThresholds": { + "gpm": { + "okMin": 0, + "okMax": 29, + "minValue": 0, + "maxValue": 35 + }, + "psi": { + "okMin": 30, + "okMax": 80, + "minValue": 0, + "maxValue": 100 + }, + "lpm": { + "okMin": 0, + "okMax": 110, + "minValue": 0, + "maxValue": 130 + }, + "kPa": { + "okMin": 210, + "okMax": 550, + "minValue": 0, + "maxValue": 700 + }, + "tempF": { + "okMin": 50, + "okMax": 80, + "minValue": 0, + "maxValue": 100 + }, + "tempC": { + "okMin": 10, + "okMax": 30, + "minValue": 0, + "maxValue": 40 + } + }, + "serialNumber": "xxxxx", + "connectivity": { + "rssi": -61, + "ssid": "IP freely" + }, + "telemetry": { + "current": { + "gpm": 0, + "psi": 78.9000015258789, + "tempF": 57, + "updated": "2020-01-16T19:01:59Z" + } + }, + "shutoff": { + "scheduledAt": "1970-01-01T00:00:00.000Z" + }, + "actionRules": [], + "location": { + "id": "mmnnoopp" + } + } + ], + "userRoles": [ + { + "userId": "12345abcde", + "roles": [ + "owner" + ] + } + ], + "address": "123 Main Street", + "city": "Boston", + "state": "MA", + "country": "us", + "postalCode": "12345", + "timezone": "US/Eastern", + "gallonsPerDayGoal": 240, + "occupants": 2, + "stories": 2, + "isProfileComplete": true, + "nickname": "Home", + "irrigationSchedule": { + "isEnabled": false + }, + "systemMode": { + "target": "home" + }, + "locationType": "sfh", + "locationSize": "lte_4000_sq_ft", + "waterShutoffKnown": "unsure", + "indoorAmenities": [], + "outdoorAmenities": [], + "plumbingAppliances": [ + "exp_tank" + ], + "notifications": { + "pending": { + "infoCount": 0, + "warningCount": 1, + "criticalCount": 0, + "alarmCount": [ + { + "id": 57, + "severity": "warning", + "count": 1 + } + ] + } + }, + "areas": { + "default": [ + { + "id": "xxxx", + "name": "Attic" + }, + { + "id": "xxxx", + "name": "Basement" + }, + { + "id": "xxxx", + "name": "Garage" + }, + { + "id": "xxxx", + "name": "Main Floor" + }, + { + "id": "xxxx", + "name": "Upstairs" + } + ], + "custom": [] + }, + "account": { + "id": "aabbccdd" + } +} diff --git a/tests/fixtures/flo/user_info_base_response.json b/tests/fixtures/flo/user_info_base_response.json new file mode 100644 index 00000000000..646b62ee834 --- /dev/null +++ b/tests/fixtures/flo/user_info_base_response.json @@ -0,0 +1,34 @@ +{ + "id": "12345abcde", + "email": "email@address.com", + "isActive": true, + "firstName": "Tom", + "lastName": "Jones", + "unitSystem": "imperial_us", + "phoneMobile": "+1 123-456-7890", + "locale": "en-US", + "locations": [ + { + "id": "mmnnoopp" + } + ], + "alarmSettings": [], + "locationRoles": [ + { + "locationId": "mmnnoopp", + "roles": [ + "owner" + ] + } + ], + "accountRole": { + "accountId": "aabbccdd", + "roles": [ + "owner" + ] + }, + "account": { + "id": "aabbccdd" + }, + "enabledFeatures": [] +} diff --git a/tests/fixtures/flo/user_info_expand_locations_response.json b/tests/fixtures/flo/user_info_expand_locations_response.json new file mode 100644 index 00000000000..829596b6849 --- /dev/null +++ b/tests/fixtures/flo/user_info_expand_locations_response.json @@ -0,0 +1,120 @@ +{ + "id": "12345abcde", + "email": "email@address.com", + "isActive": true, + "firstName": "Tom", + "lastName": "Jones", + "unitSystem": "imperial_us", + "phoneMobile": "+1 123-456-7890", + "locale": "en-US", + "locations": [ + { + "id": "mmnnoopp", + "users": [ + { + "id": "12345abcde" + } + ], + "devices": [ + { + "id": "98765", + "macAddress": "606405c11e10" + } + ], + "userRoles": [ + { + "userId": "12345abcde", + "roles": [ + "owner" + ] + } + ], + "address": "123 Main Stree", + "city": "Boston", + "state": "MA", + "country": "us", + "postalCode": "12345", + "timezone": "US/Easter", + "gallonsPerDayGoal": 240, + "occupants": 2, + "stories": 2, + "isProfileComplete": true, + "nickname": "Home", + "irrigationSchedule": { + "isEnabled": false + }, + "systemMode": { + "target": "home" + }, + "locationType": "sfh", + "locationSize": "lte_4000_sq_ft", + "waterShutoffKnown": "unsure", + "indoorAmenities": [], + "outdoorAmenities": [], + "plumbingAppliances": [ + "exp_tank" + ], + "notifications": { + "pending": { + "infoCount": 0, + "warningCount": 1, + "criticalCount": 0, + "alarmCount": [ + { + "id": 57, + "severity": "warning", + "count": 1 + } + ] + } + }, + "areas": { + "default": [ + { + "id": "xxxxx", + "name": "Attic" + }, + { + "id": "xxxxx", + "name": "Basement" + }, + { + "id": "xxxxx", + "name": "Garage" + }, + { + "id": "xxxxx", + "name": "Main Floor" + }, + { + "id": "xxxxx", + "name": "Upstairs" + } + ], + "custom": [] + }, + "account": { + "id": "aabbccdd" + } + } + ], + "alarmSettings": [], + "locationRoles": [ + { + "locationId": "mmnnoopp", + "roles": [ + "owner" + ] + } + ], + "accountRole": { + "accountId": "aabbccdd", + "roles": [ + "owner" + ] + }, + "account": { + "id": "aabbccdd" + }, + "enabledFeatures": [] +} diff --git a/tests/fixtures/flo/water_consumption_info_response.json b/tests/fixtures/flo/water_consumption_info_response.json new file mode 100644 index 00000000000..ea173da98ea --- /dev/null +++ b/tests/fixtures/flo/water_consumption_info_response.json @@ -0,0 +1,34 @@ +{ + "params": { + "startDate": "2020-01-16T07:00:00.000Z", + "endDate": "2020-01-17T06:59:59.999Z", + "interval": "1h", + "tz": "US/Mountain", + "locationId": "mmnnoopp" + }, + "aggregations": { + "sumTotalGallonsConsumed": 3.674 + }, + "items": [ + { + "time": "2020-01-16T00:00:00-07:00", + "gallonsConsumed": 0.04 + }, + { + "time": "2020-01-16T01:00:00-07:00", + "gallonsConsumed": 0.477 + }, + { + "time": "2020-01-16T03:00:00-07:00", + "gallonsConsumed": 0.442 + }, + { + "time": "2020-01-16T07:00:00-07:00", + "gallonsConsumed": 1.216 + }, + { + "time": "2020-01-16T08:00:00-07:00", + "gallonsConsumed": 1.499 + } + ] +} diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 9b2a4380cae..71358cdd973 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -249,7 +249,7 @@ class AiohttpClientMockResponse: """Return mock response as a string.""" return self.response.decode(encoding) - async def json(self, encoding="utf-8"): + async def json(self, encoding="utf-8", content_type=None): """Return mock response as a json.""" return _json.loads(self.response.decode(encoding)) From f82f815304b81e99de77f628ac79434c48df5d82 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 10 Aug 2020 14:51:04 +0100 Subject: [PATCH 081/862] Add water level sensors provided by UK Environment Agency (#31954) --- CODEOWNERS | 1 + homeassistant/components/eafm/__init__.py | 23 + homeassistant/components/eafm/config_flow.py | 61 +++ homeassistant/components/eafm/const.py | 3 + homeassistant/components/eafm/manifest.json | 8 + homeassistant/components/eafm/sensor.py | 183 ++++++++ homeassistant/components/eafm/strings.json | 17 + .../components/eafm/translations/en.json | 17 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/eafm/__init__.py | 1 + tests/components/eafm/conftest.py | 18 + tests/components/eafm/test_config_flow.py | 59 +++ tests/components/eafm/test_sensor.py | 431 ++++++++++++++++++ 15 files changed, 829 insertions(+) create mode 100644 homeassistant/components/eafm/__init__.py create mode 100644 homeassistant/components/eafm/config_flow.py create mode 100644 homeassistant/components/eafm/const.py create mode 100644 homeassistant/components/eafm/manifest.json create mode 100644 homeassistant/components/eafm/sensor.py create mode 100644 homeassistant/components/eafm/strings.json create mode 100644 homeassistant/components/eafm/translations/en.json create mode 100644 tests/components/eafm/__init__.py create mode 100644 tests/components/eafm/conftest.py create mode 100644 tests/components/eafm/test_config_flow.py create mode 100644 tests/components/eafm/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c2198123382..bd76f51b4d4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -107,6 +107,7 @@ homeassistant/components/dunehd/* @bieniu homeassistant/components/dweet/* @fabaff homeassistant/components/dynalite/* @ziv1234 homeassistant/components/dyson/* @etheralm +homeassistant/components/eafm/* @Jc2k homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/edl21/* @mtdcr diff --git a/homeassistant/components/eafm/__init__.py b/homeassistant/components/eafm/__init__.py new file mode 100644 index 00000000000..f0ce5128624 --- /dev/null +++ b/homeassistant/components/eafm/__init__.py @@ -0,0 +1,23 @@ +"""UK Environment Agency Flood Monitoring Integration.""" + +from .const import DOMAIN + + +async def async_setup(hass, config): + """Set up devices.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass, entry): + """Set up flood monitoring sensors for this config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload flood monitoring sensors.""" + return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") diff --git a/homeassistant/components/eafm/config_flow.py b/homeassistant/components/eafm/config_flow.py new file mode 100644 index 00000000000..0f640951f9f --- /dev/null +++ b/homeassistant/components/eafm/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow to configure flood monitoring gauges.""" +import logging + +from aioeafm import get_stations +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +# pylint: disable=unused-import +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class UKFloodsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a UK Environment Agency flood monitoring config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Handle a UK Floods config flow.""" + self.stations = {} + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + errors = {} + + if user_input is not None: + station = self.stations[user_input["station"]] + await self.async_set_unique_id(station, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input["station"], data={"station": station}, + ) + + session = async_get_clientsession(hass=self.hass) + stations = await get_stations(session) + + self.stations = {} + for station in stations: + label = station["label"] + + # API annoyingly sometimes returns a list and some times returns a string + # E.g. L3121 has a label of ['Scurf Dyke', 'Scurf Dyke Dyke Level'] + if isinstance(label, list): + label = label[-1] + + self.stations[label] = station["stationReference"] + + if not self.stations: + return self.async_abort(reason="no_stations") + + return self.async_show_form( + step_id="user", + errors=errors, + data_schema=vol.Schema( + {vol.Required("station"): vol.In(sorted(self.stations))} + ), + ) diff --git a/homeassistant/components/eafm/const.py b/homeassistant/components/eafm/const.py new file mode 100644 index 00000000000..c87cf263a9d --- /dev/null +++ b/homeassistant/components/eafm/const.py @@ -0,0 +1,3 @@ +"""Constants for the eafm component.""" + +DOMAIN = "eafm" diff --git a/homeassistant/components/eafm/manifest.json b/homeassistant/components/eafm/manifest.json new file mode 100644 index 00000000000..66813d33036 --- /dev/null +++ b/homeassistant/components/eafm/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "eafm", + "name": "Environment Agency Flood Gauges", + "documentation": "https://www.home-assistant.io/integrations/eafm", + "config_flow": true, + "codeowners": ["@Jc2k"], + "requirements": ["aioeafm==0.1.2"] +} diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py new file mode 100644 index 00000000000..ae4968bcf31 --- /dev/null +++ b/homeassistant/components/eafm/sensor.py @@ -0,0 +1,183 @@ +"""Support for guages from flood monitoring API.""" +from datetime import timedelta +import logging + +from aioeafm import get_station +import async_timeout + +from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +UNIT_MAPPING = { + "http://qudt.org/1.1/vocab/unit#Meter": LENGTH_METERS, +} + + +def get_measures(station_data): + """Force measure key to always be a list.""" + if "measures" not in station_data: + return [] + if isinstance(station_data["measures"], dict): + return [station_data["measures"]] + return station_data["measures"] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up UK Flood Monitoring Sensors.""" + station_key = config_entry.data["station"] + session = async_get_clientsession(hass=hass) + + measurements = set() + + async def async_update_data(): + # DataUpdateCoordinator will handle aiohttp ClientErrors and timouts + async with async_timeout.timeout(30): + data = await get_station(session, station_key) + + measures = get_measures(data) + entities = [] + + # Look to see if payload contains new measures + for measure in measures: + if measure["@id"] in measurements: + continue + + if "latestReading" not in measure: + # Don't create a sensor entity for a gauge that isn't available + continue + + entities.append(Measurement(hass.data[DOMAIN][station_key], measure["@id"])) + measurements.add(measure["@id"]) + + async_add_entities(entities) + + # Turn data.measures into a dict rather than a list so easier for entities to + # find themselves. + data["measures"] = {measure["@id"]: measure for measure in measures} + + return data + + hass.data[DOMAIN][station_key] = coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="sensor", + update_method=async_update_data, + update_interval=timedelta(seconds=15 * 60), + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + +class Measurement(Entity): + """A gauge at a flood monitoring station.""" + + attribution = "This uses Environment Agency flood and river level data from the real-time data API" + + def __init__(self, coordinator, key): + """Initialise the gauge with a data instance and station.""" + self.coordinator = coordinator + self.key = key + + @property + def station_name(self): + """Return the station name for the measure.""" + return self.coordinator.data["label"] + + @property + def station_id(self): + """Return the station id for the measure.""" + return self.coordinator.data["measures"][self.key]["stationReference"] + + @property + def qualifier(self): + """Return the qualifier for the station.""" + return self.coordinator.data["measures"][self.key]["qualifier"] + + @property + def parameter_name(self): + """Return the parameter name for the station.""" + return self.coordinator.data["measures"][self.key]["parameterName"] + + @property + def name(self): + """Return the name of the gauge.""" + return f"{self.station_name} {self.parameter_name} {self.qualifier}" + + @property + def should_poll(self) -> bool: + """Stations are polled as a group - the entity shouldn't poll by itself.""" + return False + + @property + def unique_id(self): + """Return the unique id of the gauge.""" + return self.key + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, "measure-id", self.station_id)}, + "name": self.name, + "manufacturer": "https://environment.data.gov.uk/", + "model": self.parameter_name, + "entry_type": "service", + } + + @property + def available(self) -> bool: + """Return True if entity is available.""" + if not self.coordinator.last_update_success: + return False + + # If sensor goes offline it will no longer contain a reading + if "latestReading" not in self.coordinator.data["measures"][self.key]: + return False + + # Sometimes lastestReading key is present but actually a URL rather than a piece of data + # This is usually because the sensor has been archived + if not isinstance( + self.coordinator.data["measures"][self.key]["latestReading"], dict + ): + return False + + return True + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + measure = self.coordinator.data["measures"][self.key] + if "unit" not in measure: + return None + return UNIT_MAPPING.get(measure["unit"], measure["unitName"]) + + @property + def device_state_attributes(self): + """Return the sensor specific state attributes.""" + return {ATTR_ATTRIBUTION: self.attribution} + + @property + def state(self): + """Return the current sensor value.""" + return self.coordinator.data["measures"][self.key]["latestReading"]["value"] + + async def async_update(self): + """ + Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/eafm/strings.json b/homeassistant/components/eafm/strings.json new file mode 100644 index 00000000000..9c829abcd1a --- /dev/null +++ b/homeassistant/components/eafm/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "title": "Track a flood monitoring station", + "description": "Select the station you want to monitor", + "data": { + "station": "Station" + } + } + }, + "abort": { + "no_stations": "No flood monitoring stations found.", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/eafm/translations/en.json b/homeassistant/components/eafm/translations/en.json new file mode 100644 index 00000000000..3a3ddae390b --- /dev/null +++ b/homeassistant/components/eafm/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "title": "Track a flood monitoring station", + "description": "Select the station you want to monitor", + "data": { + "station": "Station" + } + } + }, + "abort": { + "no_stations": "No flood monitoring stations found.", + "already_configured": "This station is already configured." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f6171eb9f47..3698fad422c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -44,6 +44,7 @@ FLOWS = [ "doorbird", "dunehd", "dynalite", + "eafm", "ecobee", "elgato", "elkm1", diff --git a/requirements_all.txt b/requirements_all.txt index bcd39c115c7..4c4ce6de1ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -157,6 +157,9 @@ aiobotocore==0.11.1 # homeassistant.components.minecraft_server aiodns==2.0.0 +# homeassistant.components.eafm +aioeafm==0.1.2 + # homeassistant.components.esphome aioesphomeapi==2.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c1a97e6bfd..da524c6423d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -85,6 +85,9 @@ aiobotocore==0.11.1 # homeassistant.components.minecraft_server aiodns==2.0.0 +# homeassistant.components.eafm +aioeafm==0.1.2 + # homeassistant.components.esphome aioesphomeapi==2.6.1 diff --git a/tests/components/eafm/__init__.py b/tests/components/eafm/__init__.py new file mode 100644 index 00000000000..d94537dd966 --- /dev/null +++ b/tests/components/eafm/__init__.py @@ -0,0 +1 @@ +"""Tests for eafm.""" diff --git a/tests/components/eafm/conftest.py b/tests/components/eafm/conftest.py new file mode 100644 index 00000000000..b25c0f4cdba --- /dev/null +++ b/tests/components/eafm/conftest.py @@ -0,0 +1,18 @@ +"""eafm fixtures.""" + +from asynctest import patch +import pytest + + +@pytest.fixture() +def mock_get_stations(): + """Mock aioeafm.get_stations.""" + with patch("homeassistant.components.eafm.config_flow.get_stations") as patched: + yield patched + + +@pytest.fixture() +def mock_get_station(): + """Mock aioeafm.get_station.""" + with patch("homeassistant.components.eafm.sensor.get_station") as patched: + yield patched diff --git a/tests/components/eafm/test_config_flow.py b/tests/components/eafm/test_config_flow.py new file mode 100644 index 00000000000..4656e34a34c --- /dev/null +++ b/tests/components/eafm/test_config_flow.py @@ -0,0 +1,59 @@ +"""Tests for eafm config flow.""" +from asynctest import patch +import pytest +from voluptuous.error import MultipleInvalid + +from homeassistant.components.eafm import const + + +async def test_flow_no_discovered_stations(hass, mock_get_stations): + """Test config flow discovers no station.""" + mock_get_stations.return_value = [] + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "abort" + assert result["reason"] == "no_stations" + + +async def test_flow_invalid_station(hass, mock_get_stations): + """Test config flow errors on invalid station.""" + mock_get_stations.return_value = [ + {"label": "My station", "stationReference": "L12345"} + ] + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + with pytest.raises(MultipleInvalid): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"station": "My other station"} + ) + + +async def test_flow_works(hass, mock_get_stations, mock_get_station): + """Test config flow discovers no station.""" + mock_get_stations.return_value = [ + {"label": "My station", "stationReference": "L12345"} + ] + mock_get_station.return_value = [ + {"label": "My station", "stationReference": "L12345"} + ] + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + with patch("homeassistant.components.eafm.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"station": "My station"} + ) + + assert result["type"] == "create_entry" + assert result["title"] == "My station" + assert result["data"] == { + "station": "L12345", + } diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py new file mode 100644 index 00000000000..6cce7a2bc4b --- /dev/null +++ b/tests/components/eafm/test_sensor.py @@ -0,0 +1,431 @@ +"""Tests for polling measures.""" +import datetime + +import aiohttp +import pytest + +from homeassistant import config_entries +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + +DUMMY_REQUEST_INFO = aiohttp.client.RequestInfo( + url="http://example.com", method="GET", headers={}, real_url="http://example.com" +) + +CONNECTION_EXCEPTIONS = [ + aiohttp.ClientConnectionError("Mock connection error"), + aiohttp.ClientResponseError(DUMMY_REQUEST_INFO, [], message="Mock response error"), +] + + +async def async_setup_test_fixture(hass, mock_get_station, initial_value): + """Create a dummy config entry for testing polling.""" + mock_get_station.return_value = initial_value + + entry = MockConfigEntry( + version=1, + domain="eafm", + entry_id="VikingRecorder1234", + data={"station": "L1234"}, + title="Viking Recorder", + connection_class=config_entries.CONN_CLASS_CLOUD_PUSH, + ) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, "eafm", {}) + assert entry.state == config_entries.ENTRY_STATE_LOADED + await hass.async_block_till_done() + + async def poll(value): + mock_get_station.reset_mock(return_value=True, side_effect=True) + + if isinstance(value, Exception): + mock_get_station.side_effect = value + else: + mock_get_station.return_value = value + + next_update = dt_util.utcnow() + datetime.timedelta(60 * 15) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + return entry, poll + + +async def test_reading_measures_not_list(hass, mock_get_station): + """ + Test that a measure can be a dict not a list. + + E.g. https://environment.data.gov.uk/flood-monitoring/id/stations/751110 + """ + _ = await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + }, + }, + ) + + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + + +async def test_reading_no_unit(hass, mock_get_station): + """ + Test that a sensor functions even if its unit is not known. + + E.g. https://environment.data.gov.uk/flood-monitoring/id/stations/L0410 + """ + _ = await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + } + ], + }, + ) + + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + + +async def test_ignore_invalid_latest_reading(hass, mock_get_station): + """ + Test that a sensor functions even if its unit is not known. + + E.g. https://environment.data.gov.uk/flood-monitoring/id/stations/L0410 + """ + _ = await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": "http://environment.data.gov.uk/flood-monitoring/data/readings/L0410-level-stage-i-15_min----/2017-02-22T10-30-00Z", + "stationReference": "L0410", + }, + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Other", + "latestReading": {"value": 5}, + "stationReference": "L0411", + }, + ], + }, + ) + + state = hass.states.get("sensor.my_station_water_level_stage") + assert state is None + + state = hass.states.get("sensor.my_station_other_stage") + assert state.state == "5" + + +@pytest.mark.parametrize("exception", CONNECTION_EXCEPTIONS) +async def test_reading_unavailable(hass, mock_get_station, exception): + """Test that a sensor is marked as unavailable if there is a connection error.""" + _, poll = await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + } + ], + }, + ) + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + + await poll(exception) + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "unavailable" + + +@pytest.mark.parametrize("exception", CONNECTION_EXCEPTIONS) +async def test_recover_from_failure(hass, mock_get_station, exception): + """Test that a sensor recovers from failures.""" + _, poll = await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + } + ], + }, + ) + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + + await poll(exception) + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "unavailable" + + await poll( + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 56}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + } + ], + }, + ) + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "56" + + +async def test_reading_is_sampled(hass, mock_get_station): + """Test that a sensor is added and polled.""" + await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + } + ], + }, + ) + + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + assert state.attributes["unit_of_measurement"] == "m" + + +async def test_multiple_readings_are_sampled(hass, mock_get_station): + """Test that multiple sensors are added and polled.""" + await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + }, + { + "@id": "really-long-unique-id-2", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Second Stage", + "parameterName": "Water Level", + "latestReading": {"value": 4}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + }, + ], + }, + ) + + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + assert state.attributes["unit_of_measurement"] == "m" + + state = hass.states.get("sensor.my_station_water_level_second_stage") + assert state.state == "4" + assert state.attributes["unit_of_measurement"] == "m" + + +async def test_ignore_no_latest_reading(hass, mock_get_station): + """Test that a measure is ignored if it has no latest reading.""" + await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + }, + { + "@id": "really-long-unique-id-2", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Second Stage", + "parameterName": "Water Level", + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + }, + ], + }, + ) + + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + assert state.attributes["unit_of_measurement"] == "m" + + state = hass.states.get("sensor.my_station_water_level_second_stage") + assert state is None + + +async def test_mark_existing_as_unavailable_if_no_latest(hass, mock_get_station): + """Test that a measure is marked as unavailable if it has no latest reading.""" + _, poll = await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + } + ], + }, + ) + + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + assert state.attributes["unit_of_measurement"] == "m" + + await poll( + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + } + ], + } + ) + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "unavailable" + + await poll( + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + } + ], + } + ) + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + + +async def test_unload_entry(hass, mock_get_station): + """Test being able to unload an entry.""" + entry, _ = await async_setup_test_fixture( + hass, + mock_get_station, + { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + } + ], + }, + ) + + # And there should be an entity + state = hass.states.get("sensor.my_station_water_level_stage") + assert state.state == "5" + + assert await entry.async_unload(hass) + + # And the entity should be gone + assert not hass.states.get("sensor.my_station_water_level_stage") From dba82fedd46a4a7e11b9fd25e6c398326bdef238 Mon Sep 17 00:00:00 2001 From: Markus Bong Date: Mon, 10 Aug 2020 15:57:04 +0200 Subject: [PATCH 082/862] Bump devolo-home-control-api to 0.13.0 (#38718) --- .../components/devolo_home_control/binary_sensor.py | 2 +- .../components/devolo_home_control/devolo_device.py | 4 ++-- .../devolo_home_control/devolo_multi_level_switch.py | 2 +- .../components/devolo_home_control/manifest.json | 2 +- homeassistant/components/devolo_home_control/sensor.py | 2 +- homeassistant/components/devolo_home_control/switch.py | 8 ++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index 26c27f59ae8..d3da333b407 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -58,7 +58,7 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): self._binary_sensor_property.sub_type or self._binary_sensor_property.sensor_type ) - name = device_instance.itemName + name = device_instance.item_name if self._device_class is None: if device_instance.binary_sensor_property.get(element_uid).sub_type != "": diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 1694aeb3f47..06ddf2175f7 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -32,7 +32,7 @@ class DevoloDeviceEntity(Entity): async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self.subscriber = Subscriber( - self._device_instance.itemName, callback=self.sync_callback + self._device_instance.item_name, callback=self.sync_callback ) self._homecontrol.publisher.register( self._device_instance.uid, self.subscriber, self.sync_callback @@ -54,7 +54,7 @@ class DevoloDeviceEntity(Entity): """Return the device info.""" return { "identifiers": {(DOMAIN, self._device_instance.uid)}, - "name": self._device_instance.itemName, + "name": self._device_instance.item_name, "manufacturer": self._brand, "model": self._model, } diff --git a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py index 897899e725c..70629854dea 100644 --- a/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py +++ b/homeassistant/components/devolo_home_control/devolo_multi_level_switch.py @@ -15,7 +15,7 @@ class DevoloMultiLevelSwitchDeviceEntity(DevoloDeviceEntity): homecontrol=homecontrol, device_instance=device_instance, element_uid=element_uid, - name=f"{device_instance.itemName}", + name=device_instance.item_name, sync=self._sync, ) self._multi_level_switch_property = device_instance.multi_level_switch_property[ diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index 1ee54f23fde..bdb3a80fff6 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -2,7 +2,7 @@ "domain": "devolo_home_control", "name": "devolo Home Control", "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", - "requirements": ["devolo-home-control-api==0.11.0"], + "requirements": ["devolo-home-control-api==0.13.0"], "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], "quality_scale": "silver" diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index 31bee42e4a2..bdf0f05528d 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -54,7 +54,7 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity): self._multi_level_sensor_property.sensor_type ) - name = device_instance.itemName + name = device_instance.item_name if self._device_class is None: name += f" {self._multi_level_sensor_property.sensor_type}" diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index 4ba212af379..9a7812af7bd 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -43,7 +43,9 @@ class DevoloSwitch(SwitchEntity): self._unique_id = element_uid self._homecontrol = homecontrol - self._name = self._device_instance.itemName + self._name = self._device_instance.item_name + + # This is not doing I/O. It fetches an internal state of the API self._available = self._device_instance.is_online() # Get the brand and model information @@ -66,7 +68,9 @@ class DevoloSwitch(SwitchEntity): async def async_added_to_hass(self): """Call when entity is added to hass.""" - self.subscriber = Subscriber(self._device_instance.itemName, callback=self.sync) + self.subscriber = Subscriber( + self._device_instance.item_name, callback=self.sync + ) self._homecontrol.publisher.register( self._device_instance.uid, self.subscriber, self.sync ) diff --git a/requirements_all.txt b/requirements_all.txt index 4c4ce6de1ba..6378ae5418e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -479,7 +479,7 @@ deluge-client==1.7.1 denonavr==0.9.4 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.11.0 +devolo-home-control-api==0.13.0 # homeassistant.components.directv directv==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da524c6423d..4793e48d391 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -249,7 +249,7 @@ defusedxml==0.6.0 denonavr==0.9.4 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.11.0 +devolo-home-control-api==0.13.0 # homeassistant.components.directv directv==0.3.0 From ff539f3f05240772be0221a0260d46c638a8ef0f Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Mon, 10 Aug 2020 17:30:59 +0300 Subject: [PATCH 083/862] Add Dynalite current preset service (#38689) Co-authored-by: Martin Hjelmare --- homeassistant/components/dynalite/__init__.py | 37 ++++++++++- homeassistant/components/dynalite/const.py | 1 + .../components/dynalite/manifest.json | 2 +- .../components/dynalite/services.yaml | 13 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dynalite/test_init.py | 65 +++++++++++++++++++ 7 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/dynalite/services.yaml diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 78281f56f0f..f04869d7160 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -1,7 +1,7 @@ """Support for the Dynalite networks.""" import asyncio -from typing import Any, Dict, Union +from typing import Any, Dict, List, Union import voluptuous as vol @@ -9,7 +9,7 @@ from homeassistant import config_entries from homeassistant.components.cover import DEVICE_CLASSES_SCHEMA from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -19,6 +19,9 @@ from .const import ( ACTIVE_INIT, ACTIVE_OFF, ACTIVE_ON, + ATTR_AREA, + ATTR_CHANNEL, + ATTR_HOST, CONF_ACTIVE, CONF_AREA, CONF_AUTO_DISCOVER, @@ -198,6 +201,36 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: ) ) + def get_bridges(host: str) -> List[DynaliteBridge]: + result = [] + for entry_id in hass.data[DOMAIN]: + cur_bridge = hass.data[DOMAIN][entry_id] + if not host or cur_bridge.host == host: + result.append(cur_bridge) + return result + + async def request_area_preset_service(service_call: ServiceCall): + host = service_call.data.get(ATTR_HOST, "") + bridges = get_bridges(host) + LOGGER.debug("Selected bridged for service call: %s", bridges) + area = service_call.data[ATTR_AREA] + channel = service_call.data.get(ATTR_CHANNEL) + for bridge in bridges: + bridge.dynalite_devices.request_area_preset(area, channel) + + hass.services.async_register( + DOMAIN, + "request_area_preset", + request_area_preset_service, + vol.Schema( + { + vol.Optional(ATTR_HOST): cv.string, + vol.Required(ATTR_AREA): int, + vol.Optional(ATTR_CHANNEL): int, + } + ), + ) + return True diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index 373e64a1b76..be8d94b6e46 100644 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -51,6 +51,7 @@ DEFAULT_TEMPLATES = { } ATTR_AREA = "area" +ATTR_CHANNEL = "channel" ATTR_HOST = "host" ATTR_PACKET = "packet" ATTR_PRESET = "preset" diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index a8277eea85c..9c733ccc7a2 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -4,5 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.44"] + "requirements": ["dynalite_devices==0.1.45"] } diff --git a/homeassistant/components/dynalite/services.yaml b/homeassistant/components/dynalite/services.yaml new file mode 100644 index 00000000000..ccaa06ed19a --- /dev/null +++ b/homeassistant/components/dynalite/services.yaml @@ -0,0 +1,13 @@ +request_area_preset: + description: "Requests Dynalite to report the preset for an area." + fields: + host: + description: "Host gateway IP to send to or all configured gateways if not specified." + example: "192.168.0.101" + area: + description: "Area to request the preset reported" + example: 2 + channel: + description: "Channel to request the preset to be reported from. Default is channel 1" + example: 1 + diff --git a/requirements_all.txt b/requirements_all.txt index 6378ae5418e..c6fc71ff78e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ dsmr_parser==0.18 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.44 +dynalite_devices==0.1.45 # homeassistant.components.rainforest_eagle eagle200_reader==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4793e48d391..68bf6eb0bee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -264,7 +264,7 @@ doorbirdpy==2.1.0 dsmr_parser==0.18 # homeassistant.components.dynalite -dynalite_devices==0.1.44 +dynalite_devices==0.1.45 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index be646a1854d..6d2fd66f1a2 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -79,6 +79,71 @@ async def test_async_setup(hass): assert len(hass.config_entries.async_entries(dynalite.DOMAIN)) == 1 +async def test_service_request_area_preset(hass): + """Test requesting and area preset via service call.""" + with patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=True, + ), patch( + "dynalite_devices_lib.dynalite.Dynalite.request_area_preset", return_value=True, + ) as mock_req_area_pres: + assert await async_setup_component( + hass, + dynalite.DOMAIN, + { + dynalite.DOMAIN: { + dynalite.CONF_BRIDGES: [ + { + CONF_HOST: "1.2.3.4", + CONF_PORT: 1234, + dynalite.CONF_AREA: {"7": {CONF_NAME: "test"}}, + }, + {CONF_HOST: "5.6.7.8", CONF_PORT: 5678}, + ] + } + }, + ) + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries(dynalite.DOMAIN)) == 2 + await hass.services.async_call( + dynalite.DOMAIN, "request_area_preset", {"host": "1.2.3.4", "area": 2}, + ) + await hass.async_block_till_done() + mock_req_area_pres.assert_called_once_with(2, 1) + mock_req_area_pres.reset_mock() + await hass.services.async_call( + dynalite.DOMAIN, "request_area_preset", {"area": 3}, + ) + await hass.async_block_till_done() + assert mock_req_area_pres.mock_calls == [call(3, 1), call(3, 1)] + mock_req_area_pres.reset_mock() + await hass.services.async_call( + dynalite.DOMAIN, "request_area_preset", {"host": "5.6.7.8", "area": 4}, + ) + await hass.async_block_till_done() + mock_req_area_pres.assert_called_once_with(4, 1) + mock_req_area_pres.reset_mock() + await hass.services.async_call( + dynalite.DOMAIN, "request_area_preset", {"host": "6.5.4.3", "area": 5}, + ) + await hass.async_block_till_done() + mock_req_area_pres.assert_not_called() + mock_req_area_pres.reset_mock() + await hass.services.async_call( + dynalite.DOMAIN, + "request_area_preset", + {"host": "1.2.3.4", "area": 6, "channel": 9}, + ) + await hass.async_block_till_done() + mock_req_area_pres.assert_called_once_with(6, 9) + mock_req_area_pres.reset_mock() + await hass.services.async_call( + dynalite.DOMAIN, "request_area_preset", {"host": "1.2.3.4", "area": 7}, + ) + await hass.async_block_till_done() + mock_req_area_pres.assert_called_once_with(7, 1) + + async def test_async_setup_bad_config1(hass): """Test a successful with bad config on templates.""" with patch( From c6105580bf3c72ac7ed76bea3237a9b2f544185d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 10 Aug 2020 16:34:52 +0200 Subject: [PATCH 084/862] Add scikit-build to installed env (#38726) --- azure-pipelines-wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index c8943595429..3e7821d77af 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -48,7 +48,7 @@ jobs: parameters: builderVersion: '$(versionWheels)' builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' - builderPip: 'Cython;numpy' + builderPip: 'Cython;numpy;scikit-build' skipBinary: 'aiohttp' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' From 3d0ea42ac00fad7681eab67bde6e9a8160eb6d00 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Aug 2020 17:50:03 +0200 Subject: [PATCH 085/862] Add current device class to WLED current sensor (#38687) --- homeassistant/components/wled/sensor.py | 6 ++++++ tests/components/wled/test_sensor.py | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index b684bdc0977..8cfd9f98f02 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from typing import Any, Callable, Dict, List, Optional, Union +from homeassistant.components.sensor import DEVICE_CLASS_CURRENT from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DATA_BYTES, @@ -105,6 +106,11 @@ class WLEDEstimatedCurrentSensor(WLEDSensor): """Return the state of the sensor.""" return self.coordinator.data.info.leds.power + @property + def device_class(self) -> Optional[str]: + """Return the class of this sensor.""" + return DEVICE_CLASS_CURRENT + class WLEDUptimeSensor(WLEDSensor): """Defines a WLED uptime sensor.""" diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 66fedfdd274..adbd2e6e1fc 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -3,7 +3,10 @@ from datetime import datetime import pytest -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + DEVICE_CLASS_CURRENT, + DOMAIN as SENSOR_DOMAIN, +) from homeassistant.components.wled.const import ( ATTR_LED_COUNT, ATTR_MAX_POWER, @@ -12,6 +15,7 @@ from homeassistant.components.wled.const import ( SIGNAL_DBM, ) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DATA_BYTES, @@ -94,6 +98,7 @@ async def test_sensors( assert state.attributes.get(ATTR_LED_COUNT) == 30 assert state.attributes.get(ATTR_MAX_POWER) == 850 assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENT_MA + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_CURRENT assert state.state == "470" entry = registry.async_get("sensor.wled_rgb_light_estimated_current") From 52729e9dc8ff54f1ad55b168f30f4e547488bf4a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 10 Aug 2020 17:54:46 +0200 Subject: [PATCH 086/862] Add scan_tag webhook to mobile app (#38721) --- .../components/mobile_app/webhook.py | 12 +++++++++ tests/components/mobile_app/test_webhook.py | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index ca9c31011ed..d03505b0cb9 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -538,3 +538,15 @@ async def webhook_get_config(hass, config_entry, data): pass return webhook_response(resp, registration=config_entry.data) + + +@WEBHOOK_COMMANDS.register("scan_tag") +@validate_schema({vol.Required("tag_id"): cv.string}) +async def webhook_scan_tag(hass, config_entry, data): + """Handle a fire event webhook.""" + hass.bus.async_fire( + "tag_scanned", + {"tag_id": data["tag_id"], "device_id": config_entry.data[ATTR_DEVICE_ID]}, + context=registration_context(config_entry.data), + ) + return empty_okay_response() diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 195c60d830c..bd38bca535b 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -406,3 +406,28 @@ async def test_webhook_camera_stream_stream_available_but_errors( webhook_json = await resp.json() assert webhook_json["hls_path"] is None assert webhook_json["mjpeg_path"] == "/api/camera_proxy_stream/camera.stream_camera" + + +async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_client): + """Test that we can scan tags.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen("tag_scanned", store_event) + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + json={"type": "scan_tag", "data": {"tag_id": "mock-tag-id"}}, + ) + + assert resp.status == 200 + json = await resp.json() + assert json == {} + + assert len(events) == 1 + assert events[0].data["tag_id"] == "mock-tag-id" + assert events[0].data["device_id"] == "mock-device-id" From ea65eb270f561456e65b3fb13e4b2ea621803e18 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 10 Aug 2020 18:00:02 +0200 Subject: [PATCH 087/862] Ignore requirements for env_canada (#38731) --- requirements_all.txt | 2 +- script/gen_requirements_all.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index c6fc71ff78e..7f979564444 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -545,7 +545,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -env_canada==0.2.0 +# env_canada==0.2.0 # homeassistant.components.envirophat # envirophat==0.0.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 772b9af5034..b851983b6f6 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -22,6 +22,7 @@ COMMENT_REQUIREMENTS = ( "bme680", "credstash", "decora", + "env_canada", "envirophat", "evdev", "face_recognition", From bbda1272c2b35fc3b5a6c399d6050cacc4d0ff83 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Mon, 10 Aug 2020 15:32:00 -0500 Subject: [PATCH 088/862] Bump pysmartthings 0.7.3 (#38732) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index bf137ae398d..58ea833cb7d 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -3,7 +3,7 @@ "name": "SmartThings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smartthings", - "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.2"], + "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.3"], "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@andrewsayre"] diff --git a/requirements_all.txt b/requirements_all.txt index 7f979564444..85fd5f57130 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1635,7 +1635,7 @@ pysmappee==0.2.9 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.7.2 +pysmartthings==0.7.3 # homeassistant.components.smarty pysmarty==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68bf6eb0bee..08c34a48ee6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -764,7 +764,7 @@ pysmappee==0.2.9 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.7.2 +pysmartthings==0.7.3 # homeassistant.components.soma pysoma==0.0.10 From cf72ade09357919beee3be8d6e0fc2d0d153ec95 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 10 Aug 2020 15:51:37 -0500 Subject: [PATCH 089/862] Add URL as common string (#38694) * add URL as common string * Update strings.json --- homeassistant/components/huawei_lte/strings.json | 2 +- homeassistant/strings.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 554ec0f53ca..8435d0a5347 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -21,7 +21,7 @@ "user": { "data": { "password": "[%key:common::config_flow::data::password%]", - "url": "URL", + "url": "[%key:common::config_flow::data::url%]", "username": "[%key:common::config_flow::data::username%]" }, "description": "Enter device access details. Specifying username and password is optional, but enables support for more integration features. On the other hand, use of an authorized connection may cause problems accessing the device web interface from outside Home Assistant while the integration is active, and the other way around.", diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 26622feadfb..63615094715 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -31,6 +31,7 @@ "host": "Host", "ip": "IP Address", "port": "Port", + "url": "URL", "usb_path": "USB Device Path", "access_token": "Access Token", "api_key": "API Key" From 844b3f8d233a79d80e6b50afe86662c306509486 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 10 Aug 2020 17:40:07 -0400 Subject: [PATCH 090/862] Make default duration 1/10th of a second for ZHA light calls (#38739) * default duration to 1/10th of a second * update test --- homeassistant/components/zha/light.py | 2 +- tests/components/zha/test_light.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 51b0633ecf2..ff080562190 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -192,7 +192,7 @@ class BaseLight(LogMixin, light.LightEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) - duration = transition * 10 if transition else 0 + duration = transition * 10 if transition else 1 brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 6b94354ed59..bd340445527 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -403,7 +403,7 @@ async def async_test_level_on_off_from_hass( 4, (zigpy.types.uint8_t, zigpy.types.uint16_t), 10, - 0, + 1, expect_reply=True, manufacturer=None, tsn=None, From 8555e17eb94d4edf9866ea0c08024b45c8177312 Mon Sep 17 00:00:00 2001 From: Vaclav <44951610+bruxy70@users.noreply.github.com> Date: Tue, 11 Aug 2020 03:22:39 +0200 Subject: [PATCH 091/862] Add hourly forecast to met.no (#38700) * Add hourly forecast * fix tests to assert for 2 entities created * fix test to assert for 4 calls * correct test tracking home number of calls * fox tests * fix test * Apply suggestions from code review * black Co-authored-by: Chris Talkington --- homeassistant/components/met/__init__.py | 8 ++++--- homeassistant/components/met/weather.py | 27 ++++++++++++++++------- tests/components/met/test_weather.py | 12 +++++----- tests/components/onboarding/test_views.py | 2 +- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 32b3e939c43..ae994bfc396 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -22,7 +22,7 @@ import homeassistant.util.dt as dt_util from .const import CONF_TRACK_HOME, DOMAIN -URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/" +URL = "https://api.met.no/weatherapi/locationforecast/2.0/classic" _LOGGER = logging.getLogger(__name__) @@ -116,7 +116,8 @@ class MetWeatherData: self._is_metric = is_metric self._weather_data = None self.current_weather_data = {} - self.forecast_data = None + self.daily_forecast = None + self.hourly_forecast = None def init_data(self): """Weather data inialization - get the coordinates.""" @@ -149,5 +150,6 @@ class MetWeatherData: await self._weather_data.fetching_data() self.current_weather_data = self._weather_data.get_current_weather() time_zone = dt_util.DEFAULT_TIME_ZONE - self.forecast_data = self._weather_data.get_forecast(time_zone) + self.daily_forecast = self._weather_data.get_forecast(time_zone, False) + self.hourly_forecast = self._weather_data.get_forecast(time_zone, True) return self diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 9761a2c4034..e2827367757 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -66,18 +66,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Add a weather entity from a config_entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - [MetWeather(coordinator, config_entry.data, hass.config.units.is_metric)] + [ + MetWeather( + coordinator, config_entry.data, hass.config.units.is_metric, False + ), + MetWeather( + coordinator, config_entry.data, hass.config.units.is_metric, True + ), + ] ) class MetWeather(WeatherEntity): """Implementation of a Met.no weather condition.""" - def __init__(self, coordinator, config, is_metric): + def __init__(self, coordinator, config, is_metric, hourly): """Initialise the platform with a data instance and site.""" self._config = config self._coordinator = coordinator self._is_metric = is_metric + self._hourly = hourly + self._name_appendix = "-hourly" if hourly else "" async def async_added_to_hass(self): """Start fetching data.""" @@ -103,9 +112,9 @@ class MetWeather(WeatherEntity): def unique_id(self): """Return unique ID.""" if self.track_home: - return "home" + return f"home{self._name_appendix}" - return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}" + return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{self._name_appendix}" @property def name(self): @@ -113,12 +122,12 @@ class MetWeather(WeatherEntity): name = self._config.get(CONF_NAME) if name is not None: - return name + return f"{name}{self._name_appendix}" if self.track_home: - return self.hass.config.location_name + return f"{self.hass.config.location_name}{self._name_appendix}" - return DEFAULT_NAME + return f"{DEFAULT_NAME}{self._name_appendix}" @property def condition(self): @@ -173,4 +182,6 @@ class MetWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return self._coordinator.data.forecast_data + if self._hourly: + return self._coordinator.data.hourly_forecast + return self._coordinator.data.daily_forecast diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 4577146c270..064335a998c 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -5,14 +5,14 @@ async def test_tracking_home(hass, mock_weather): """Test we track home.""" await hass.config_entries.flow.async_init("met", context={"source": "onboarding"}) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 1 - assert len(mock_weather.mock_calls) == 3 + assert len(hass.states.async_entity_ids("weather")) == 2 + assert len(mock_weather.mock_calls) == 4 # Test we track config await hass.config.async_update(latitude=10, longitude=20) await hass.async_block_till_done() - assert len(mock_weather.mock_calls) == 6 + assert len(mock_weather.mock_calls) == 8 entry = hass.config_entries.async_entries()[0] await hass.config_entries.async_remove(entry.entry_id) @@ -27,14 +27,14 @@ async def test_not_tracking_home(hass, mock_weather): data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0}, ) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 1 - assert len(mock_weather.mock_calls) == 3 + assert len(hass.states.async_entity_ids("weather")) == 2 + assert len(mock_weather.mock_calls) == 4 # Test we do not track config await hass.config.async_update(latitude=10, longitude=20) await hass.async_block_till_done() - assert len(mock_weather.mock_calls) == 3 + assert len(mock_weather.mock_calls) == 4 entry = hass.config_entries.async_entries()[0] await hass.config_entries.async_remove(entry.entry_id) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 0d425642622..8b8f9761bdb 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -276,4 +276,4 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): assert resp.status == 200 await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 1 + assert len(hass.states.async_entity_ids("weather")) == 2 From 39843319e28f043c8e9e17bf3f4c2bcc4629cd5a Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 11 Aug 2020 03:55:44 +0100 Subject: [PATCH 092/862] Update IPMA weather component (#38697) * long overdue mismatch * missing updated tests --- homeassistant/components/ipma/weather.py | 10 +++++----- tests/components/ipma/test_weather.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 62f1b0b39af..a2c47f43ffb 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, @@ -256,9 +256,9 @@ class IPMAWeather(WeatherEntity): None, ), ATTR_FORECAST_TEMP: float(data_in.feels_like_temperature), - ATTR_FORECAST_PRECIPITATION: ( - data_in.precipitation_probability - if float(data_in.precipitation_probability) >= 0 + ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( + int(float(data_in.precipitation_probability)) + if int(float(data_in.precipitation_probability)) >= 0 else None ), ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, @@ -281,7 +281,7 @@ class IPMAWeather(WeatherEntity): ), ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, ATTR_FORECAST_TEMP: data_in.max_temperature, - ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: data_in.precipitation_probability, ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index c7a8bbda63f..bfe7eefbb4c 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -5,7 +5,7 @@ from homeassistant.components import weather from homeassistant.components.weather import ( ATTR_FORECAST, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, @@ -192,7 +192,7 @@ async def test_daily_forecast(hass): assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" assert forecast.get(ATTR_FORECAST_TEMP) == 16.2 assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6 - assert forecast.get(ATTR_FORECAST_PRECIPITATION) == "100.0" + assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == "100.0" assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "10" assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" @@ -216,6 +216,6 @@ async def test_hourly_forecast(hass): forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" assert forecast.get(ATTR_FORECAST_TEMP) == 7.7 - assert forecast.get(ATTR_FORECAST_PRECIPITATION) == "80.0" + assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 80.0 assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "32.7" assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" From 0d51d8660ea2d74518dadc3bf51a6cef0f7d4e9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Aug 2020 02:45:36 -0500 Subject: [PATCH 093/862] Install a threading.excepthook on python 3.8 and later (#38741) Exceptions in threads were being silently discarded and never logged as the new in python 3.8 threading.excepthook was not being set. --- homeassistant/bootstrap.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 4cf95d68f05..a7953cbec6c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -6,6 +6,7 @@ import logging import logging.handlers import os import sys +import threading from time import monotonic from typing import TYPE_CHECKING, Any, Dict, Optional, Set @@ -308,6 +309,12 @@ def async_enable_logging( "Uncaught exception", exc_info=args # type: ignore ) + if sys.version_info[:2] >= (3, 8): + threading.excepthook = lambda args: logging.getLogger(None).exception( + "Uncaught thread exception", + exc_info=(args.exc_type, args.exc_value, args.exc_traceback), + ) + # Log errors to a file if we have write access to file or config dir if log_file is None: err_log_path = hass.config.path(ERROR_LOG_FILENAME) From 9bc68b9a73b9d8a04d8103238c959457c415cfff Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Tue, 11 Aug 2020 10:17:34 +0200 Subject: [PATCH 094/862] Remove prezzibenzina integration (ADR-0004) (#38736) --- .coveragerc | 1 - .../components/prezzibenzina/__init__.py | 1 - .../components/prezzibenzina/manifest.json | 7 -- .../components/prezzibenzina/sensor.py | 119 ------------------ requirements_all.txt | 3 - 5 files changed, 131 deletions(-) delete mode 100644 homeassistant/components/prezzibenzina/__init__.py delete mode 100644 homeassistant/components/prezzibenzina/manifest.json delete mode 100644 homeassistant/components/prezzibenzina/sensor.py diff --git a/.coveragerc b/.coveragerc index 6b4133bf476..3bf9397a80a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -655,7 +655,6 @@ omit = homeassistant/components/poolsense/__init__.py homeassistant/components/poolsense/sensor.py homeassistant/components/poolsense/binary_sensor.py - homeassistant/components/prezzibenzina/sensor.py homeassistant/components/proliphix/climate.py homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py diff --git a/homeassistant/components/prezzibenzina/__init__.py b/homeassistant/components/prezzibenzina/__init__.py deleted file mode 100644 index af68e845bbc..00000000000 --- a/homeassistant/components/prezzibenzina/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The prezzibenzina component.""" diff --git a/homeassistant/components/prezzibenzina/manifest.json b/homeassistant/components/prezzibenzina/manifest.json deleted file mode 100644 index 5aa4a6ec77f..00000000000 --- a/homeassistant/components/prezzibenzina/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "prezzibenzina", - "name": "Prezzi Benzina", - "documentation": "https://www.home-assistant.io/integrations/prezzibenzina", - "requirements": ["prezzibenzina-py==1.1.4"], - "codeowners": [] -} diff --git a/homeassistant/components/prezzibenzina/sensor.py b/homeassistant/components/prezzibenzina/sensor.py deleted file mode 100644 index f45d9d84669..00000000000 --- a/homeassistant/components/prezzibenzina/sensor.py +++ /dev/null @@ -1,119 +0,0 @@ -"""Support for the PrezziBenzina.it service.""" -import datetime as dt -from datetime import timedelta -import logging - -from prezzibenzina import PrezziBenzinaPy -import voluptuous as vol - -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_TIME, CONF_NAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -ATTR_FUEL = "fuel" -ATTR_SERVICE = "service" -ATTRIBUTION = "Data provided by PrezziBenzina.it" - -CONF_STATION = "station" -CONF_TYPES = "fuel_types" - -ICON = "mdi:fuel" - -FUEL_TYPES = [ - "Benzina", - "Benzina speciale", - "Diesel", - "Diesel speciale", - "GPL", - "Metano", -] - -SCAN_INTERVAL = timedelta(minutes=120) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_STATION): cv.string, - vol.Optional(CONF_NAME, None): cv.string, - vol.Optional(CONF_TYPES, None): vol.All(cv.ensure_list, [vol.In(FUEL_TYPES)]), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the PrezziBenzina sensor platform.""" - - station = config[CONF_STATION] - name = config.get(CONF_NAME) - types = config.get(CONF_TYPES) - - client = PrezziBenzinaPy() - dev = [] - info = client.get_by_id(station) - - if name is None: - name = client.get_station_name(station) - - for index, info in enumerate(info): - if types is not None and info["fuel"] not in types: - continue - dev.append( - PrezziBenzinaSensor( - index, client, station, name, info["fuel"], info["service"] - ) - ) - - add_entities(dev, True) - - -class PrezziBenzinaSensor(Entity): - """Implementation of a PrezziBenzina sensor.""" - - def __init__(self, index, client, station, name, ft, srv): - """Initialize the PrezziBenzina sensor.""" - self._client = client - self._index = index - self._data = None - self._station = station - self._name = f"{name} {ft} {srv}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - @property - def state(self): - """Return the state of the device.""" - return self._data["price"].replace(" €", "") - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._data["price"].split(" ")[1] - - @property - def device_state_attributes(self): - """Return the device state attributes of the last update.""" - timestamp = dt.datetime.strptime( - self._data["date"], "%d/%m/%Y %H:%M" - ).isoformat() - - attrs = { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_FUEL: self._data["fuel"], - ATTR_SERVICE: self._data["service"], - ATTR_TIME: timestamp, - } - return attrs - - def update(self): - """Get the latest data and updates the states.""" - self._data = self._client.get_by_id(self._station)[self._index] diff --git a/requirements_all.txt b/requirements_all.txt index 85fd5f57130..c3e9ee21b96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1116,9 +1116,6 @@ praw==6.5.1 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 -# homeassistant.components.prezzibenzina -prezzibenzina-py==1.1.4 - # homeassistant.components.proliphix proliphix==0.4.1 From bd3f0750609969a165f421d96b0e88fd7ad21b9c Mon Sep 17 00:00:00 2001 From: Yehuda Davis Date: Tue, 11 Aug 2020 04:31:38 -0400 Subject: [PATCH 095/862] Add Ecobee services (#38749) Enable/disable automatic daylight savings time Enable/disable the Alexa mic on the Ecobee 4 Enable/disable Smart Home/Away and follow me modes --- homeassistant/components/ecobee/climate.py | 45 +++++++++++++++++++ homeassistant/components/ecobee/services.yaml | 33 ++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 4aeb52b60ff..dcb21ac41b1 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -33,6 +33,7 @@ from homeassistant.const import ( STATE_ON, TEMP_FAHRENHEIT, ) +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.util.temperature import convert @@ -49,6 +50,10 @@ ATTR_RESUME_ALL = "resume_all" ATTR_START_DATE = "start_date" ATTR_START_TIME = "start_time" ATTR_VACATION_NAME = "vacation_name" +ATTR_DST_ENABLED = "dst_enabled" +ATTR_MIC_ENABLED = "mic_enabled" +ATTR_AUTO_AWAY = "auto_away" +ATTR_FOLLOW_ME = "follow_me" DEFAULT_RESUME_ALL = False PRESET_TEMPERATURE = "temp" @@ -98,6 +103,9 @@ SERVICE_CREATE_VACATION = "create_vacation" SERVICE_DELETE_VACATION = "delete_vacation" SERVICE_RESUME_PROGRAM = "resume_program" SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time" +SERVICE_SET_DST_MODE = "set_dst_mode" +SERVICE_SET_MIC_MODE = "set_mic_mode" +SERVICE_SET_OCCUPANCY_MODES = "set_occupancy_modes" DTGROUP_INCLUSIVE_MSG = ( f"{ATTR_START_DATE}, {ATTR_START_TIME}, {ATTR_END_DATE}, " @@ -165,6 +173,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(devices, True) + platform = entity_platform.current_platform.get() + def create_vacation_service(service): """Create a vacation on the target thermostat.""" entity_id = service.data[ATTR_ENTITY_ID] @@ -248,6 +258,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): schema=RESUME_PROGRAM_SCHEMA, ) + platform.async_register_entity_service( + SERVICE_SET_DST_MODE, + {vol.Required(ATTR_DST_ENABLED): cv.boolean}, + "set_dst_mode", + ) + + platform.async_register_entity_service( + SERVICE_SET_MIC_MODE, + {vol.Required(ATTR_MIC_ENABLED): cv.boolean}, + "set_mic_mode", + ) + + platform.async_register_entity_service( + SERVICE_SET_OCCUPANCY_MODES, + { + vol.Optional(ATTR_AUTO_AWAY): cv.boolean, + vol.Optional(ATTR_FOLLOW_ME): cv.boolean, + }, + "set_occupancy_modes", + ) + class Thermostat(ClimateEntity): """A thermostat class for Ecobee.""" @@ -720,3 +751,17 @@ class Thermostat(ClimateEntity): self._last_active_hvac_mode, ) self.set_hvac_mode(self._last_active_hvac_mode) + + def set_dst_mode(self, dst_enabled): + """Enable/disable automatic daylight savings time.""" + self.data.ecobee.set_dst_mode(self.thermostat_index, dst_enabled) + + def set_mic_mode(self, mic_enabled): + """Enable/disable Alexa mic (only for Ecobee 4).""" + self.data.ecobee.set_mic_mode(self.thermostat_index, mic_enabled) + + def set_occupancy_modes(self, auto_away=None, follow_me=None): + """Enable/disable Smart Home/Away and Follow Me modes.""" + self.data.ecobee.set_occupancy_modes( + self.thermostat_index, auto_away, follow_me + ) diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index 88137bd9530..dd848d09d56 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -69,3 +69,36 @@ set_fan_min_on_time: fan_min_on_time: description: New value of fan min on time. example: 5 + +set_dst_mode: + description: Enable/disable automatic daylight savings time. + fields: + entity_id: + description: Name(s) of entities to change. + example: "climate.kitchen" + dst_enabled: + description: Enable automatic daylight savings time. + example: "true" + +set_mic_mode: + description: Enable/disable Alexa mic (only for Ecobee 4). + fields: + entity_id: + description: Name(s) of entities to change. + example: "climate.kitchen" + mic_enabled: + description: Enable Alexa mic. + example: "true" + +set_occupancy_modes: + description: Enable/disable Smart Home/Away and Follow Me modes. + fields: + entity_id: + description: Name(s) of entities to change. + example: "climate.kitchen" + auto_away: + description: Enable Smart Home/Away mode. + example: "true" + follow_me: + description: Enable Follow Me mode. + example: "true" From 227d7c0a99dbe0e8e5eafafebd2aa2fd0cfdb509 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 11 Aug 2020 05:08:47 -0400 Subject: [PATCH 096/862] [RFC] Add Tag integration (#38727) Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 1 + .../components/default_config/manifest.json | 1 + homeassistant/components/tag/__init__.py | 124 ++++++++++++++++++ homeassistant/components/tag/const.py | 3 + homeassistant/components/tag/manifest.json | 12 ++ homeassistant/components/tag/strings.json | 3 + .../components/tag/translations/en.json | 3 + tests/components/tag/__init__.py | 1 + tests/components/tag/test_init.py | 98 ++++++++++++++ 9 files changed, 246 insertions(+) create mode 100644 homeassistant/components/tag/__init__.py create mode 100644 homeassistant/components/tag/const.py create mode 100644 homeassistant/components/tag/manifest.json create mode 100644 homeassistant/components/tag/strings.json create mode 100644 homeassistant/components/tag/translations/en.json create mode 100644 tests/components/tag/__init__.py create mode 100644 tests/components/tag/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index bd76f51b4d4..0525541a838 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -412,6 +412,7 @@ homeassistant/components/synology_dsm/* @ProtoThis @Quentame homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff homeassistant/components/tado/* @michaelarnauts @bdraco +homeassistant/components/tag/* @balloob @dmulcahey homeassistant/components/tahoma/* @philklei homeassistant/components/tankerkoenig/* @guillempages homeassistant/components/tautulli/* @ludeeus diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 338aeb2e285..78da3e1ff50 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -16,6 +16,7 @@ "ssdp", "sun", "system_health", + "tag", "updater", "zeroconf", "zone", diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py new file mode 100644 index 00000000000..968b74e226d --- /dev/null +++ b/homeassistant/components/tag/__init__.py @@ -0,0 +1,124 @@ +"""The Tag integration.""" +import logging +import typing + +import voluptuous as vol + +from homeassistant.const import CONF_ID, CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import collection +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import Store +from homeassistant.loader import bind_hass +import homeassistant.util.dt as dt_util + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) +DEVICE_ID = "device_id" +EVENT_TAG_SCANNED = "tag_scanned" +LAST_SCANNED = "last_scanned" +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 +TAG_ID = "tag_id" +TAGS = "tags" + +CREATE_FIELDS = { + vol.Required(CONF_ID): cv.string, + vol.Optional(TAG_ID): cv.string, + vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional("description"): cv.string, + vol.Optional(LAST_SCANNED): cv.datetime, +} + +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional("description"): cv.string, + vol.Optional(LAST_SCANNED): cv.datetime, +} + + +class TagIDExistsError(HomeAssistantError): + """Raised when an item is not found.""" + + def __init__(self, item_id: str): + """Initialize tag id exists error.""" + super().__init__(f"Tag with id: {item_id} already exists.") + self.item_id = item_id + + +class TagIDManager(collection.IDManager): + """ID manager for tags.""" + + def generate_id(self, suggestion: str) -> str: + """Generate an ID.""" + if self.has_id(suggestion): + raise TagIDExistsError(suggestion) + + return suggestion + + +class TagStorageCollection(collection.StorageCollection): + """Tag collection stored in storage.""" + + CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + + async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + """Validate the config is valid.""" + if TAG_ID in data: + data[CONF_ID] = data.pop(TAG_ID) + data = self.CREATE_SCHEMA(data) + # make last_scanned JSON serializeable + if LAST_SCANNED in data: + data[LAST_SCANNED] = str(data[LAST_SCANNED]) + return data + + @callback + def _get_suggested_id(self, info: typing.Dict) -> str: + """Suggest an ID based on the config.""" + return info[CONF_ID] + + async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + """Return a new updated data object.""" + data = {**data, **self.UPDATE_SCHEMA(update_data)} + # make last_scanned JSON serializeable + if LAST_SCANNED in data: + data[LAST_SCANNED] = str(data[LAST_SCANNED]) + return data + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Tag component.""" + hass.data[DOMAIN] = {} + id_manager = TagIDManager() + hass.data[DOMAIN][TAGS] = storage_collection = TagStorageCollection( + Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}_storage_collection"), + id_manager, + ) + await storage_collection.async_load() + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) + return True + + +@bind_hass +async def async_scan_tag(hass, tag_id, device_id, context=None): + """Handle when a tag is scanned.""" + if DOMAIN not in hass.config.components: + raise HomeAssistantError("tag component has not been set up.") + + hass.bus.async_fire( + EVENT_TAG_SCANNED, {TAG_ID: tag_id, DEVICE_ID: device_id}, context=context + ) + helper = hass.data[DOMAIN][TAGS] + if tag_id in helper.data: + await helper.async_update_item(tag_id, {LAST_SCANNED: dt_util.utcnow()}) + else: + await helper.async_create_item( + {CONF_ID: tag_id, LAST_SCANNED: dt_util.utcnow()} + ) + _LOGGER.debug("Tag: %s scanned by device: %s", tag_id, device_id) diff --git a/homeassistant/components/tag/const.py b/homeassistant/components/tag/const.py new file mode 100644 index 00000000000..60f6e05c27e --- /dev/null +++ b/homeassistant/components/tag/const.py @@ -0,0 +1,3 @@ +"""Constants for the Tag integration.""" + +DOMAIN = "tag" diff --git a/homeassistant/components/tag/manifest.json b/homeassistant/components/tag/manifest.json new file mode 100644 index 00000000000..d330fdaf3f8 --- /dev/null +++ b/homeassistant/components/tag/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "tag", + "name": "Tag", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/tag", + "requirements": [], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@balloob", "@dmulcahey"] +} diff --git a/homeassistant/components/tag/strings.json b/homeassistant/components/tag/strings.json new file mode 100644 index 00000000000..ba680ba0d81 --- /dev/null +++ b/homeassistant/components/tag/strings.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} diff --git a/homeassistant/components/tag/translations/en.json b/homeassistant/components/tag/translations/en.json new file mode 100644 index 00000000000..fdac700612d --- /dev/null +++ b/homeassistant/components/tag/translations/en.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/tests/components/tag/__init__.py b/tests/components/tag/__init__.py new file mode 100644 index 00000000000..5908bd04e59 --- /dev/null +++ b/tests/components/tag/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tag integration.""" diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py new file mode 100644 index 00000000000..d10a59ef2f0 --- /dev/null +++ b/tests/components/tag/test_init.py @@ -0,0 +1,98 @@ +"""Tests for the tag component.""" +import logging + +import pytest + +from homeassistant.components.tag import DOMAIN, TAGS, async_scan_tag +from homeassistant.helpers import collection +from homeassistant.setup import async_setup_component + +_LOGGER = logging.getLogger(__name__) + + +@pytest.fixture +def storage_setup(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": [{"id": "test tag"}]}, + } + else: + hass_storage[DOMAIN] = items + config = {DOMAIN: {}} + return await async_setup_component(hass, DOMAIN, config) + + return _storage + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing tags via WS.""" + assert await storage_setup() + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert "test tag" in result + + +async def test_tag_scanned(hass, hass_ws_client, storage_setup): + """Test scanning tags.""" + assert await storage_setup() + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert "test tag" in result + + await async_scan_tag(hass, "new tag", "some_scanner") + await client.send_json({"id": 7, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 2 + assert "test tag" in result + assert "new tag" in result + assert result["new tag"]["last_scanned"] is not None + + +def track_changes(coll: collection.ObservableCollection): + """Create helper to track changes in a collection.""" + changes = [] + + async def listener(*args): + changes.append(args) + + coll.async_add_listener(listener) + + return changes + + +async def test_tag_id_exists(hass, hass_ws_client, storage_setup): + """Test scanning tags.""" + assert await storage_setup() + changes = track_changes(hass.data[DOMAIN][TAGS]) + client = await hass_ws_client(hass) + + await client.send_json({"id": 2, "type": f"{DOMAIN}/create", "tag_id": "test tag"}) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "unknown_error" + assert len(changes) == 0 From 6a2466794499d781b8470c05aea4637c6148b096 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 11 Aug 2020 08:13:40 -0400 Subject: [PATCH 097/862] Update Flo config flow and associated tests (#38722) --- homeassistant/components/flo/config_flow.py | 11 +++--- homeassistant/components/flo/entity.py | 3 +- homeassistant/components/flo/sensor.py | 2 +- tests/components/flo/conftest.py | 2 -- tests/components/flo/test_config_flow.py | 38 ++++++++++++++++----- tests/components/flo/test_sensor.py | 26 +++++++++++++- 6 files changed, 62 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py index 1f8e5fc08bd..24208aa16a8 100644 --- a/homeassistant/components/flo/config_flow.py +++ b/homeassistant/components/flo/config_flow.py @@ -27,9 +27,8 @@ async def validate_input(hass: core.HomeAssistant, data): api = await async_get_api( data[CONF_USERNAME], data[CONF_PASSWORD], session=session ) - except RequestError: - raise CannotConnect - except Exception: # pylint: disable=broad-except + except RequestError as request_error: + _LOGGER.error("Error connecting to the Flo API: %s", request_error) raise CannotConnect user_info = await api.user.get_info() @@ -48,15 +47,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() try: info = await validate_input(self.hass, user_input) - return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: errors["base"] = "cannot_connect" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 10ffa835454..35c6e022dcf 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -60,10 +60,11 @@ class FloEntity(Entity): @property def should_poll(self) -> bool: """Poll state from device.""" - return True + return False async def async_update(self): """Update Flo entity.""" + await self._device.async_request_refresh() async def async_added_to_hass(self): """When entity is added to hass.""" diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index 2cbc43e8cd8..cac259f475f 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.extend([FloCurrentFlowRateSensor(device) for device in devices]) entities.extend([FloTemperatureSensor(device) for device in devices]) entities.extend([FloPressureSensor(device) for device in devices]) - async_add_entities(entities, True) + async_add_entities(entities) class FloDailyUsageSensor(FloEntity): diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py index 5790d3d4eb3..69167b58a02 100644 --- a/tests/components/flo/conftest.py +++ b/tests/components/flo/conftest.py @@ -25,9 +25,7 @@ def config_entry(hass): @pytest.fixture def aioclient_mock_fixture(aioclient_mock): """Fixture to provide a aioclient mocker.""" - now = round(time.time()) - # Mocks the login response for flo. aioclient_mock.post( "https://api.meetflo.com/api/v1/users/auth", diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index bddea76e73c..265f2ae2d38 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -1,7 +1,12 @@ """Test the flo config flow.""" +import json +import time + from homeassistant import config_entries, setup from homeassistant.components.flo.const import DOMAIN +from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID + from tests.async_mock import patch @@ -20,20 +25,37 @@ async def test_form(hass, aioclient_mock_fixture): "homeassistant.components.flo.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"username": "test-username", "password": "test-password"}, + result["flow_id"], {"username": TEST_USER_ID, "password": TEST_PASSWORD} ) - assert result2["type"] == "create_entry" - assert result2["title"] == "Home" - assert result2["data"] == {"username": "test-username", "password": "test-password"} - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 + assert result2["type"] == "create_entry" + assert result2["title"] == "Home" + assert result2["data"] == {"username": TEST_USER_ID, "password": TEST_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, aioclient_mock): """Test we handle cannot connect error.""" + now = round(time.time()) + # Mocks a failed login response for flo. + aioclient_mock.post( + "https://api.meetflo.com/api/v1/users/auth", + json=json.dumps( + { + "token": TEST_TOKEN, + "tokenPayload": { + "user": {"user_id": TEST_USER_ID, "email": TEST_EMAIL_ADDRESS}, + "timestamp": now, + }, + "tokenExpiration": 86400, + "timeNow": now, + } + ), + headers={"Content-Type": "application/json"}, + status=400, + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index 5db1fdacfe1..ab5132bd34e 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -1,6 +1,6 @@ """Test Flo by Moen sensor entities.""" from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component from .common import TEST_PASSWORD, TEST_USER_ID @@ -22,3 +22,27 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture): assert hass.states.get("sensor.water_flow_rate").state == "0" assert hass.states.get("sensor.water_pressure").state == "54.2" assert hass.states.get("sensor.water_temperature").state == "21.1" + + +async def test_manual_update_entity( + hass, config_entry, aioclient_mock_fixture, aioclient_mock +): + """Test manual update entity via service homeasasistant/update_entity.""" + config_entry.add_to_hass(hass) + assert await async_setup_component( + hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} + ) + await hass.async_block_till_done() + + assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + + await async_setup_component(hass, "homeassistant", {}) + + call_count = aioclient_mock.call_count + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.current_system_mode"]}, + blocking=True, + ) + assert aioclient_mock.call_count == call_count + 2 From 1d9a469f8477ac41cee50282e7844ae3e8748b7c Mon Sep 17 00:00:00 2001 From: etheralm Date: Tue, 11 Aug 2020 14:19:10 +0200 Subject: [PATCH 098/862] Bump dyson upstream library version (#38756) --- homeassistant/components/dyson/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 35a76180e2e..94a29d1615d 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -2,7 +2,7 @@ "domain": "dyson", "name": "Dyson", "documentation": "https://www.home-assistant.io/integrations/dyson", - "requirements": ["libpurecool==0.6.1"], + "requirements": ["libpurecool==0.6.3"], "after_dependencies": ["zeroconf"], "codeowners": ["@etheralm"] } diff --git a/requirements_all.txt b/requirements_all.txt index c3e9ee21b96..1677cf432e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -830,7 +830,7 @@ konnected==1.1.0 lakeside==0.12 # homeassistant.components.dyson -libpurecool==0.6.1 +libpurecool==0.6.3 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08c34a48ee6..123f57790ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -409,7 +409,7 @@ keyrings.alt==3.4.0 konnected==1.1.0 # homeassistant.components.dyson -libpurecool==0.6.1 +libpurecool==0.6.3 # homeassistant.components.mikrotik librouteros==3.0.0 From d1780b8d7e441b7b5a81a67952225f97ecad781b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Aug 2020 14:23:47 +0200 Subject: [PATCH 099/862] Mobile App integration to use tag integration (#38757) --- homeassistant/components/mobile_app/manifest.json | 2 +- homeassistant/components/mobile_app/webhook.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 61e90e6bd8e..732a2495311 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": ["PyNaCl==1.3.0", "emoji==0.5.4"], - "dependencies": ["http", "webhook", "person"], + "dependencies": ["http", "webhook", "person", "tag"], "after_dependencies": ["cloud", "camera"], "codeowners": ["@robbiet480"], "quality_scale": "internal" diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index d03505b0cb9..e7ff49f71f2 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -8,6 +8,7 @@ from aiohttp.web import HTTPBadRequest, Request, Response, json_response from nacl.secret import SecretBox import voluptuous as vol +from homeassistant.components import tag from homeassistant.components.binary_sensor import ( DEVICE_CLASSES as BINARY_SENSOR_CLASSES, ) @@ -544,9 +545,10 @@ async def webhook_get_config(hass, config_entry, data): @validate_schema({vol.Required("tag_id"): cv.string}) async def webhook_scan_tag(hass, config_entry, data): """Handle a fire event webhook.""" - hass.bus.async_fire( - "tag_scanned", - {"tag_id": data["tag_id"], "device_id": config_entry.data[ATTR_DEVICE_ID]}, - context=registration_context(config_entry.data), + await tag.async_scan_tag( + hass, + data["tag_id"], + config_entry.data[ATTR_DEVICE_ID], + registration_context(config_entry.data), ) return empty_okay_response() From 3fc2bae49ef07cea270ffddbc24e7a758aade806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 11 Aug 2020 15:45:07 +0200 Subject: [PATCH 100/862] Bump frontend to 20200811.0 (#38760) --- 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 aaab3ac570c..da11f574ade 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200807.1"], + "requirements": ["home-assistant-frontend==20200811.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6b4deee3a3e..e709bf68aa4 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.35.0 -home-assistant-frontend==20200807.1 +home-assistant-frontend==20200811.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.1 netdisco==2.8.1 diff --git a/requirements_all.txt b/requirements_all.txt index 1677cf432e7..7b6289da54a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -739,7 +739,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200807.1 +home-assistant-frontend==20200811.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 123f57790ab..82ab7d01408 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -368,7 +368,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200807.1 +home-assistant-frontend==20200811.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 52b80a08c9eb3f4bed037549b11248745d4a9102 Mon Sep 17 00:00:00 2001 From: Brian Rogers Date: Tue, 11 Aug 2020 10:40:18 -0400 Subject: [PATCH 101/862] Bump Rachiopy version to 0.1.4 (#38761) --- homeassistant/components/rachio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index c289d754006..61148aae438 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -2,7 +2,7 @@ "domain": "rachio", "name": "Rachio", "documentation": "https://www.home-assistant.io/integrations/rachio", - "requirements": ["rachiopy==0.1.3"], + "requirements": ["rachiopy==0.1.4"], "dependencies": ["http"], "after_dependencies": ["cloud"], "codeowners": ["@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 7b6289da54a..e776c45b94b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1864,7 +1864,7 @@ qnapstats==0.3.0 quantum-gateway==0.0.5 # homeassistant.components.rachio -rachiopy==0.1.3 +rachiopy==0.1.4 # homeassistant.components.radiotherm radiotherm==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82ab7d01408..265926e6a06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ pywebpush==1.9.2 pyzerproc==0.2.5 # homeassistant.components.rachio -rachiopy==0.1.3 +rachiopy==0.1.4 # homeassistant.components.rainmachine regenmaschine==2.1.0 From 5802d65ef71697e6627b82e1677894d13d0f16d7 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 11 Aug 2020 11:14:02 -0400 Subject: [PATCH 102/862] Bump ZHA quirks lib to 0.0.43 (#38762) --- 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 974cac32c33..3b123c53598 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "bellows==0.18.0", "pyserial==3.4", - "zha-quirks==0.0.42", + "zha-quirks==0.0.43", "zigpy-cc==0.4.4", "zigpy-deconz==0.9.2", "zigpy==0.22.2", diff --git a/requirements_all.txt b/requirements_all.txt index e776c45b94b..b32e9a90640 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2278,7 +2278,7 @@ zengge==0.2 zeroconf==0.28.0 # homeassistant.components.zha -zha-quirks==0.0.42 +zha-quirks==0.0.43 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 265926e6a06..0ed7703d6f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1017,7 +1017,7 @@ yeelight==0.5.2 zeroconf==0.28.0 # homeassistant.components.zha -zha-quirks==0.0.42 +zha-quirks==0.0.43 # homeassistant.components.zha zigpy-cc==0.4.4 From 016cd8f8efee805729e1cb32b062be8aaab4ea81 Mon Sep 17 00:00:00 2001 From: stephan192 Date: Tue, 11 Aug 2020 17:55:50 +0200 Subject: [PATCH 103/862] Move DwdWeatherWarningsAPI to a library hosted on PyPI (#34820) * Move DwdWeatherWarningsAPI to a library hosted on PyPI PyPI library uses new DWD WFS API WFS API allows a more detailed query with reduced data sent as return Change CONF_REGION_NAME from Optional to Required because it was never really optional Set attribute region_state to "N/A" because it is not available via the new API Add attributes warning_i_parameters and warning_i_color * Use constants instead of raw strings Streamline methods state and device_state_attributes * Wrap api, use UTC time * Update homeassistant/components/dwd_weather_warnings/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/dwd_weather_warnings/manifest.json Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + .../dwd_weather_warnings/manifest.json | 4 +- .../components/dwd_weather_warnings/sensor.py | 219 ++++++------------ requirements_all.txt | 3 + 4 files changed, 81 insertions(+), 146 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0525541a838..a92eac1940d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -104,6 +104,7 @@ homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 @bdraco homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dunehd/* @bieniu +homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95 homeassistant/components/dweet/* @fabaff homeassistant/components/dynalite/* @ziv1234 homeassistant/components/dyson/* @etheralm diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index 52173f001e7..e67fbb08e29 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -2,6 +2,6 @@ "domain": "dwd_weather_warnings", "name": "Deutsche Wetter Dienst (DWD) Weather Warnings", "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", - "after_dependencies": ["rest"], - "codeowners": [] + "codeowners": ["@runningman84", "@stephan192", "@Hummel95"], + "requirements": ["dwdwfsapi==1.0.2"] } diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 152c757424c..79beebb005d 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -10,37 +10,52 @@ Warnungen vor markantem Wetter (Stufe 2) Wetterwarnungen (Stufe 1) """ from datetime import timedelta -import json import logging +from dwdwfsapi import DwdWeatherWarningsAPI import voluptuous as vol -from homeassistant.components.rest.sensor import RestData from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME -from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Data provided by DWD" +ATTR_REGION_NAME = "region_name" +ATTR_REGION_ID = "region_id" +ATTR_LAST_UPDATE = "last_update" +ATTR_WARNING_COUNT = "warning_count" + +API_ATTR_WARNING_NAME = "event" +API_ATTR_WARNING_TYPE = "event_code" +API_ATTR_WARNING_LEVEL = "level" +API_ATTR_WARNING_HEADLINE = "headline" +API_ATTR_WARNING_DESCRIPTION = "description" +API_ATTR_WARNING_INSTRUCTION = "instruction" +API_ATTR_WARNING_START = "start_time" +API_ATTR_WARNING_END = "end_time" +API_ATTR_WARNING_PARAMETERS = "parameters" +API_ATTR_WARNING_COLOR = "color" DEFAULT_NAME = "DWD-Weather-Warnings" CONF_REGION_NAME = "region_name" +CURRENT_WARNING_SENSOR = "current_warning_level" +ADVANCE_WARNING_SENSOR = "advance_warning_level" + SCAN_INTERVAL = timedelta(minutes=15) MONITORED_CONDITIONS = { - "current_warning_level": [ + CURRENT_WARNING_SENSOR: [ "Current Warning Level", None, "mdi:close-octagon-outline", ], - "advance_warning_level": [ + ADVANCE_WARNING_SENSOR: [ "Advance Warning Level", None, "mdi:close-octagon-outline", @@ -49,7 +64,7 @@ MONITORED_CONDITIONS = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_REGION_NAME): cv.string, + vol.Required(CONF_REGION_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional( CONF_MONITORED_CONDITIONS, default=list(MONITORED_CONDITIONS) @@ -63,12 +78,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) region_name = config.get(CONF_REGION_NAME) - api = DwdWeatherWarningsAPI(region_name) + api = WrappedDwDWWAPI(DwdWeatherWarningsAPI(region_name)) - sensors = [ - DwdWeatherWarningsSensor(api, name, condition) - for condition in config[CONF_MONITORED_CONDITIONS] - ] + sensors = [] + for sensor_type in config[CONF_MONITORED_CONDITIONS]: + sensors.append(DwdWeatherWarningsSensor(api, name, sensor_type)) add_entities(sensors, True) @@ -76,179 +90,96 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DwdWeatherWarningsSensor(Entity): """Representation of a DWD-Weather-Warnings sensor.""" - def __init__(self, api, name, variable): + def __init__(self, api, name, sensor_type): """Initialize a DWD-Weather-Warnings sensor.""" self._api = api self._name = name - self._var_id = variable - - variable_info = MONITORED_CONDITIONS[variable] - self._var_name = variable_info[0] - self._var_units = variable_info[1] - self._var_icon = variable_info[2] + self._sensor_type = sensor_type @property def name(self): """Return the name of the sensor.""" - return f"{self._name} {self._var_name}" + return f"{self._name} {MONITORED_CONDITIONS[self._sensor_type][0]}" @property def icon(self): """Icon to use in the frontend, if any.""" - return self._var_icon + return MONITORED_CONDITIONS[self._sensor_type][2] @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._var_units + return MONITORED_CONDITIONS[self._sensor_type][1] @property def state(self): """Return the state of the device.""" - try: - return round(self._api.data[self._var_id], 2) - except TypeError: - return self._api.data[self._var_id] + if self._sensor_type == CURRENT_WARNING_SENSOR: + return self._api.api.current_warning_level + return self._api.api.expected_warning_level @property def device_state_attributes(self): """Return the state attributes of the DWD-Weather-Warnings.""" - data = {ATTR_ATTRIBUTION: ATTRIBUTION, "region_name": self._api.region_name} + data = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_REGION_NAME: self._api.api.warncell_name, + ATTR_REGION_ID: self._api.api.warncell_id, + ATTR_LAST_UPDATE: self._api.api.last_update, + } - if self._api.region_id is not None: - data["region_id"] = self._api.region_id - - if self._api.region_state is not None: - data["region_state"] = self._api.region_state - - if self._api.data["time"] is not None: - data["last_update"] = dt_util.as_local( - dt_util.utc_from_timestamp(self._api.data["time"] / 1000) - ) - - if self._var_id == "current_warning_level": - prefix = "current" - elif self._var_id == "advance_warning_level": - prefix = "advance" + if self._sensor_type == CURRENT_WARNING_SENSOR: + searched_warnings = self._api.api.current_warnings else: - raise Exception("Unknown warning type") + searched_warnings = self._api.api.expected_warnings - data["warning_count"] = self._api.data[f"{prefix}_warning_count"] - i = 0 - for event in self._api.data[f"{prefix}_warnings"]: - i = i + 1 + data[ATTR_WARNING_COUNT] = len(searched_warnings) - # dictionary for the attribute containing the complete warning as json - event_json = event.copy() + for i, warning in enumerate(searched_warnings, 1): + data[f"warning_{i}_name"] = warning[API_ATTR_WARNING_NAME] + data[f"warning_{i}_type"] = warning[API_ATTR_WARNING_TYPE] + data[f"warning_{i}_level"] = warning[API_ATTR_WARNING_LEVEL] + data[f"warning_{i}_headline"] = warning[API_ATTR_WARNING_HEADLINE] + data[f"warning_{i}_description"] = warning[API_ATTR_WARNING_DESCRIPTION] + data[f"warning_{i}_instruction"] = warning[API_ATTR_WARNING_INSTRUCTION] + data[f"warning_{i}_start"] = warning[API_ATTR_WARNING_START] + data[f"warning_{i}_end"] = warning[API_ATTR_WARNING_END] + data[f"warning_{i}_parameters"] = warning[API_ATTR_WARNING_PARAMETERS] + data[f"warning_{i}_color"] = warning[API_ATTR_WARNING_COLOR] - data[f"warning_{i}_name"] = event["event"] - data[f"warning_{i}_level"] = event["level"] - data[f"warning_{i}_type"] = event["type"] - if event["headline"]: - data[f"warning_{i}_headline"] = event["headline"] - if event["description"]: - data[f"warning_{i}_description"] = event["description"] - if event["instruction"]: - data[f"warning_{i}_instruction"] = event["instruction"] - - if event["start"] is not None: - data[f"warning_{i}_start"] = dt_util.as_local( - dt_util.utc_from_timestamp(event["start"] / 1000) - ) - event_json["start"] = data[f"warning_{i}_start"] - - if event["end"] is not None: - data[f"warning_{i}_end"] = dt_util.as_local( - dt_util.utc_from_timestamp(event["end"] / 1000) - ) - event_json["end"] = data[f"warning_{i}_end"] - - data[f"warning_{i}"] = event_json + # Dictionary for the attribute containing the complete warning + warning_copy = warning.copy() + warning_copy[API_ATTR_WARNING_START] = data[f"warning_{i}_start"] + warning_copy[API_ATTR_WARNING_END] = data[f"warning_{i}_end"] + data[f"warning_{i}"] = warning_copy return data @property def available(self): """Could the device be accessed during the last update call.""" - return self._api.available + return self._api.api.data_valid def update(self): """Get the latest data from the DWD-Weather-Warnings API.""" + _LOGGER.debug( + "Update requested for %s (%s) by %s", + self._api.api.warncell_name, + self._api.api.warncell_id, + self._sensor_type, + ) self._api.update() -class DwdWeatherWarningsAPI: - """Get the latest data and update the states.""" +class WrappedDwDWWAPI: + """Wrapper for the DWD-Weather-Warnings api.""" - def __init__(self, region_name): - """Initialize the data object.""" - resource = "https://www.dwd.de/DWD/warnungen/warnapp_landkreise/json/warnings.json?jsonp=loadWarnings" - - # a User-Agent is necessary for this rest api endpoint (#29496) - headers = {"User-Agent": HA_USER_AGENT} - - self._rest = RestData("GET", resource, None, headers, None, True) - self.region_name = region_name - self.region_id = None - self.region_state = None - self.data = None - self.available = True - self.update() + def __init__(self, api): + """Initialize a DWD-Weather-Warnings wrapper.""" + self.api = api @Throttle(SCAN_INTERVAL) def update(self): - """Get the latest data from the DWD-Weather-Warnings.""" - try: - self._rest.update() - - json_string = self._rest.data[24 : len(self._rest.data) - 2] - json_obj = json.loads(json_string) - - data = {"time": json_obj["time"]} - - for mykey, myvalue in { - "current": "warnings", - "advance": "vorabInformation", - }.items(): - - _LOGGER.debug( - "Found %d %s global DWD warnings", len(json_obj[myvalue]), mykey - ) - - data[f"{mykey}_warning_level"] = 0 - my_warnings = [] - - if self.region_id is not None: - # get a specific region_id - if self.region_id in json_obj[myvalue]: - my_warnings = json_obj[myvalue][self.region_id] - - else: - # loop through all items to find warnings, region_id - # and region_state for region_name - for key in json_obj[myvalue]: - my_region = json_obj[myvalue][key][0]["regionName"] - if my_region != self.region_name: - continue - my_warnings = json_obj[myvalue][key] - my_state = json_obj[myvalue][key][0]["stateShort"] - self.region_id = key - self.region_state = my_state - break - - # Get max warning level - maxlevel = data[f"{mykey}_warning_level"] - for event in my_warnings: - if event["level"] >= maxlevel: - data[f"{mykey}_warning_level"] = event["level"] - - data[f"{mykey}_warning_count"] = len(my_warnings) - data[f"{mykey}_warnings"] = my_warnings - - _LOGGER.debug("Found %d %s local DWD warnings", len(my_warnings), mykey) - - self.data = data - self.available = True - except TypeError: - _LOGGER.error("Unable to fetch data from DWD-Weather-Warnings") - self.available = False + """Get the latest data from the DWD-Weather-Warnings API.""" + self.api.update() + _LOGGER.debug("Update performed") diff --git a/requirements_all.txt b/requirements_all.txt index b32e9a90640..79366b864f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -505,6 +505,9 @@ dovado==0.4.1 # homeassistant.components.dsmr dsmr_parser==0.18 +# homeassistant.components.dwd_weather_warnings +dwdwfsapi==1.0.2 + # homeassistant.components.dweet dweepy==0.3.0 From f31a580caff1fa75d8ce121c28539b9d6931c79d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 11 Aug 2020 18:23:10 +0200 Subject: [PATCH 104/862] Speed up OZW availability check (#38758) * Speed up OZW availability check * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/ozw/entity.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py index ffec0feff07..39971d0c976 100644 --- a/homeassistant/components/ozw/entity.py +++ b/homeassistant/components/ozw/entity.py @@ -25,6 +25,7 @@ from .const import DOMAIN, PLATFORMS from .discovery import check_node_schema, check_value_schema _LOGGER = logging.getLogger(__name__) +OZW_READY_STATES_VALUES = {st.value for st in OZW_READY_STATES} class ZWaveDeviceEntityValues: @@ -224,9 +225,7 @@ class ZWaveDeviceEntity(Entity): """Return entity availability.""" # Use OZW Daemon status for availability. instance_status = self.values.primary.ozw_instance.get_status() - return instance_status and instance_status.status in ( - state.value for state in OZW_READY_STATES - ) + return instance_status and instance_status.status in OZW_READY_STATES_VALUES @callback def _value_changed(self, value): From 6a8378bec02a84e0730d2119a53e0599a9d1d533 Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Tue, 11 Aug 2020 19:33:16 +0300 Subject: [PATCH 105/862] Add Dynalite service to request the channel level (#38735) * added service to request the channel level * cleanup * Update homeassistant/components/dynalite/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/dynalite/__init__.py Co-authored-by: Martin Hjelmare * Update services.yaml Co-authored-by: Martin Hjelmare --- homeassistant/components/dynalite/__init__.py | 47 ++++++++++----- homeassistant/components/dynalite/const.py | 3 + .../components/dynalite/services.yaml | 13 +++++ tests/components/dynalite/test_init.py | 58 +++++++++++++++++-- 4 files changed, 99 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index f04869d7160..dd485af0441 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -1,7 +1,7 @@ """Support for the Dynalite networks.""" import asyncio -from typing import Any, Dict, List, Union +from typing import Any, Dict, Union import voluptuous as vol @@ -49,6 +49,8 @@ from .const import ( DOMAIN, ENTITY_PLATFORMS, LOGGER, + SERVICE_REQUEST_AREA_PRESET, + SERVICE_REQUEST_CHANNEL_LEVEL, ) @@ -201,27 +203,27 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: ) ) - def get_bridges(host: str) -> List[DynaliteBridge]: - result = [] - for entry_id in hass.data[DOMAIN]: - cur_bridge = hass.data[DOMAIN][entry_id] + async def dynalite_service(service_call: ServiceCall): + data = service_call.data + host = data.get(ATTR_HOST, "") + bridges = [] + for cur_bridge in hass.data[DOMAIN].values(): if not host or cur_bridge.host == host: - result.append(cur_bridge) - return result - - async def request_area_preset_service(service_call: ServiceCall): - host = service_call.data.get(ATTR_HOST, "") - bridges = get_bridges(host) + bridges.append(cur_bridge) LOGGER.debug("Selected bridged for service call: %s", bridges) - area = service_call.data[ATTR_AREA] - channel = service_call.data.get(ATTR_CHANNEL) + if service_call.service == SERVICE_REQUEST_AREA_PRESET: + bridge_attr = "request_area_preset" + elif service_call.service == SERVICE_REQUEST_CHANNEL_LEVEL: + bridge_attr = "request_channel_level" for bridge in bridges: - bridge.dynalite_devices.request_area_preset(area, channel) + getattr(bridge.dynalite_devices, bridge_attr)( + data[ATTR_AREA], data.get(ATTR_CHANNEL) + ) hass.services.async_register( DOMAIN, - "request_area_preset", - request_area_preset_service, + SERVICE_REQUEST_AREA_PRESET, + dynalite_service, vol.Schema( { vol.Optional(ATTR_HOST): cv.string, @@ -231,6 +233,19 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: ), ) + hass.services.async_register( + DOMAIN, + SERVICE_REQUEST_CHANNEL_LEVEL, + dynalite_service, + vol.Schema( + { + vol.Optional(ATTR_HOST): cv.string, + vol.Required(ATTR_AREA): int, + vol.Required(ATTR_CHANNEL): int, + } + ), + ) + return True diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index be8d94b6e46..cfe48bdc475 100644 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -55,3 +55,6 @@ ATTR_CHANNEL = "channel" ATTR_HOST = "host" ATTR_PACKET = "packet" ATTR_PRESET = "preset" + +SERVICE_REQUEST_AREA_PRESET = "request_area_preset" +SERVICE_REQUEST_CHANNEL_LEVEL = "request_channel_level" diff --git a/homeassistant/components/dynalite/services.yaml b/homeassistant/components/dynalite/services.yaml index ccaa06ed19a..afdb01bf351 100644 --- a/homeassistant/components/dynalite/services.yaml +++ b/homeassistant/components/dynalite/services.yaml @@ -11,3 +11,16 @@ request_area_preset: description: "Channel to request the preset to be reported from. Default is channel 1" example: 1 +request_channel_level: + description: "Requests Dynalite to report the level of a specific channel." + fields: + host: + description: "Host gateway IP to send to or all configured gateways if not specified." + example: "192.168.0.101" + area: + description: "Area for the requested channel" + example: 2 + channel: + description: "Channel to request the level for." + example: 1 + diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index 6d2fd66f1a2..e36e6aeba2a 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -1,6 +1,9 @@ """Test Dynalite __init__.""" +import pytest +from voluptuous import MultipleInvalid + import homeassistant.components.dynalite.const as dynalite from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM from homeassistant.setup import async_setup_component @@ -93,12 +96,8 @@ async def test_service_request_area_preset(hass): { dynalite.DOMAIN: { dynalite.CONF_BRIDGES: [ - { - CONF_HOST: "1.2.3.4", - CONF_PORT: 1234, - dynalite.CONF_AREA: {"7": {CONF_NAME: "test"}}, - }, - {CONF_HOST: "5.6.7.8", CONF_PORT: 5678}, + {CONF_HOST: "1.2.3.4"}, + {CONF_HOST: "5.6.7.8"}, ] } }, @@ -144,6 +143,53 @@ async def test_service_request_area_preset(hass): mock_req_area_pres.assert_called_once_with(7, 1) +async def test_service_request_channel_level(hass): + """Test requesting the level of a channel via service call.""" + with patch( + "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", + return_value=True, + ), patch( + "dynalite_devices_lib.dynalite.Dynalite.request_channel_level", + return_value=True, + ) as mock_req_chan_lvl: + assert await async_setup_component( + hass, + dynalite.DOMAIN, + { + dynalite.DOMAIN: { + dynalite.CONF_BRIDGES: [ + { + CONF_HOST: "1.2.3.4", + dynalite.CONF_AREA: {"7": {CONF_NAME: "test"}}, + }, + {CONF_HOST: "5.6.7.8"}, + ] + } + }, + ) + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries(dynalite.DOMAIN)) == 2 + await hass.services.async_call( + dynalite.DOMAIN, + "request_channel_level", + {"host": "1.2.3.4", "area": 2, "channel": 3}, + ) + await hass.async_block_till_done() + mock_req_chan_lvl.assert_called_once_with(2, 3) + mock_req_chan_lvl.reset_mock() + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + dynalite.DOMAIN, "request_channel_level", {"area": 3}, + ) + await hass.async_block_till_done() + mock_req_chan_lvl.assert_not_called() + await hass.services.async_call( + dynalite.DOMAIN, "request_channel_level", {"area": 4, "channel": 5}, + ) + await hass.async_block_till_done() + assert mock_req_chan_lvl.mock_calls == [call(4, 5), call(4, 5)] + + async def test_async_setup_bad_config1(hass): """Test a successful with bad config on templates.""" with patch( From 96bcbb43c42d26790fec5e2baafa14ab62b61126 Mon Sep 17 00:00:00 2001 From: Vitalii Martyniak Date: Tue, 11 Aug 2020 20:38:45 +0300 Subject: [PATCH 106/862] Add power sensor for Aqara Wall Plug (#38672) --- homeassistant/components/xiaomi_aqara/const.py | 6 ++++++ homeassistant/components/xiaomi_aqara/sensor.py | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/const.py b/homeassistant/components/xiaomi_aqara/const.py index 58932d7a0bc..0eb117cdf3c 100644 --- a/homeassistant/components/xiaomi_aqara/const.py +++ b/homeassistant/components/xiaomi_aqara/const.py @@ -53,3 +53,9 @@ BATTERY_MODELS = [ "lock.aq1", "lock.acn02", ] + +POWER_MODELS = [ + "86plug", + "ctrl_86plug", + "ctrl_86plug.aq1", +] diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 3463cded56d..471011eab6e 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -6,14 +6,16 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + POWER_WATT, TEMP_CELSIUS, UNIT_PERCENTAGE, ) from . import XiaomiDevice -from .const import BATTERY_MODELS, DOMAIN, GATEWAYS_KEY +from .const import BATTERY_MODELS, DOMAIN, GATEWAYS_KEY, POWER_MODELS _LOGGER = logging.getLogger(__name__) @@ -24,6 +26,7 @@ SENSOR_TYPES = { "lux": ["lx", None, DEVICE_CLASS_ILLUMINANCE], "pressure": ["hPa", None, DEVICE_CLASS_PRESSURE], "bed_activity": ["μm", None, None], + "load_power": [POWER_WATT, None, DEVICE_CLASS_POWER], } @@ -89,7 +92,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append( XiaomiBatterySensor(device, "Battery", gateway, config_entry) ) - + if device["model"] in POWER_MODELS: + entities.append( + XiaomiSensor( + device, "Load Power", "load_power", gateway, config_entry + ) + ) async_add_entities(entities) From dd86de3255485c7904c7d82ad467b6b4fb0e5c18 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Aug 2020 19:42:10 +0200 Subject: [PATCH 107/862] Add energy device class to Toon sensors (#38686) --- homeassistant/components/toon/const.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index c814134f767..a5f57f95cd4 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -5,7 +5,11 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PROBLEM, ) -from homeassistant.components.sensor import DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE +from homeassistant.components.sensor import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -192,7 +196,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_average", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -210,7 +214,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: True, }, @@ -219,7 +223,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -228,7 +232,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -246,7 +250,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_produced_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -255,7 +259,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_produced_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -291,7 +295,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_produced_solar", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: "mdi:solar-power", ATTR_DEFAULT_ENABLED: True, }, @@ -300,7 +304,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_to_grid_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: "mdi:solar-power", ATTR_DEFAULT_ENABLED: False, }, @@ -309,7 +313,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_from_grid_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, From 192fe58fc8de25df5530dc46054dae3125838e71 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 11 Aug 2020 15:16:28 -0500 Subject: [PATCH 108/862] Time trigger can also accept an input_datetime Entity ID (#38698) --- homeassistant/components/automation/time.py | 100 +++++++++++++++++--- tests/components/automation/test_time.py | 90 ++++++++++++++++-- 2 files changed, 170 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index f59ceff81ea..76acaf89c6c 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -1,4 +1,5 @@ """Offer time listening automation rules.""" +from datetime import datetime import logging import voluptuous as vol @@ -6,39 +7,112 @@ import voluptuous as vol from homeassistant.const import CONF_AT, CONF_PLATFORM from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import async_track_time_change +from homeassistant.helpers.event import ( + async_track_point_in_time, + async_track_state_change, + async_track_time_change, +) +import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) +_TIME_TRIGGER_SCHEMA = vol.Any( + cv.time, + vol.All(str, cv.entity_domain("input_datetime")), + msg="Expected HH:MM, HH:MM:SS or Entity ID from domain 'input_datetime'", +) + TRIGGER_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "time", - vol.Required(CONF_AT): vol.All(cv.ensure_list, [cv.time]), + vol.Required(CONF_AT): vol.All(cv.ensure_list, [_TIME_TRIGGER_SCHEMA]), } ) async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - at_times = config[CONF_AT] + entities = {} + removes = [] @callback def time_automation_listener(now): """Listen for time changes and calls action.""" hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}}) - removes = [ - async_track_time_change( - hass, - time_automation_listener, - hour=at_time.hour, - minute=at_time.minute, - second=at_time.second, - ) - for at_time in at_times - ] + @callback + def update_entity_trigger(entity_id, old_state=None, new_state=None): + # If a listener was already set up for entity, remove it. + remove = entities.get(entity_id) + if remove: + remove() + removes.remove(remove) + remove = None + + # Check state of entity. If valid, set up a listener. + if new_state: + has_date = new_state.attributes["has_date"] + if has_date: + year = new_state.attributes["year"] + month = new_state.attributes["month"] + day = new_state.attributes["day"] + has_time = new_state.attributes["has_time"] + if has_time: + hour = new_state.attributes["hour"] + minute = new_state.attributes["minute"] + second = new_state.attributes["second"] + else: + # If no time then use midnight. + hour = minute = second = 0 + + if has_date: + # If input_datetime has date, then track point in time. + trigger_dt = dt_util.DEFAULT_TIME_ZONE.localize( + datetime(year, month, day, hour, minute, second) + ) + # Only set up listener if time is now or in the future. + if trigger_dt >= dt_util.now(): + remove = async_track_point_in_time( + hass, time_automation_listener, trigger_dt + ) + elif has_time: + # Else if it has time, then track time change. + remove = async_track_time_change( + hass, + time_automation_listener, + hour=hour, + minute=minute, + second=second, + ) + + # Was a listener set up? + if remove: + removes.append(remove) + + entities[entity_id] = remove + + for at_time in config[CONF_AT]: + if isinstance(at_time, str): + # input_datetime entity + update_entity_trigger(at_time, new_state=hass.states.get(at_time)) + else: + # datetime.time + removes.append( + async_track_time_change( + hass, + time_automation_listener, + hour=at_time.hour, + minute=at_time.minute, + second=at_time.second, + ) + ) + + # Track state changes of any entities. + removes.append( + async_track_state_change(hass, list(entities), update_entity_trigger) + ) @callback def remove_track_time_changes(): diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index c8b95985636..b7540af3673 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -34,8 +34,8 @@ async def test_if_fires_using_at(hass, calls): now = dt_util.utcnow() time_that_will_not_match_right_away = now.replace( - year=now.year + 1, hour=4, minute=59, second=0 - ) + hour=4, minute=59, second=0 + ) + timedelta(2) with patch( "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away @@ -59,7 +59,7 @@ async def test_if_fires_using_at(hass, calls): now = dt_util.utcnow() async_fire_time_changed( - hass, now.replace(year=now.year + 1, hour=5, minute=0, second=0) + hass, now.replace(hour=5, minute=0, second=0) + timedelta(2) ) await hass.async_block_till_done() @@ -67,14 +67,90 @@ async def test_if_fires_using_at(hass, calls): assert calls[0].data["some"] == "time - 5" +@pytest.mark.parametrize( + "has_date,has_time", [(True, True), (True, False), (False, True)] +) +async def test_if_fires_using_at_input_datetime(hass, calls, has_date, has_time): + """Test for firing at input_datetime.""" + await async_setup_component( + hass, + "input_datetime", + {"input_datetime": {"trigger": {"has_date": has_date, "has_time": has_time}}}, + ) + + now = dt_util.now() + + trigger_dt = now.replace( + hour=5 if has_time else 0, minute=0, second=0, microsecond=0 + ) + timedelta(2) + + await hass.services.async_call( + "input_datetime", + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.trigger", + "datetime": str(trigger_dt.replace(tzinfo=None)), + }, + blocking=True, + ) + + time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) + + some_data = "{{ trigger.platform }}-{{ trigger.now.day }}-{{ trigger.now.hour }}" + with patch( + "homeassistant.util.dt.utcnow", + return_value=dt_util.as_utc(time_that_will_not_match_right_away), + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "time", "at": "input_datetime.trigger"}, + "action": { + "service": "test.automation", + "data_template": {"some": some_data}, + }, + } + }, + ) + + async_fire_time_changed(hass, trigger_dt) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == f"time-{trigger_dt.day}-{trigger_dt.hour}" + + if has_date: + trigger_dt += timedelta(days=1) + if has_time: + trigger_dt += timedelta(hours=1) + + await hass.services.async_call( + "input_datetime", + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.trigger", + "datetime": str(trigger_dt.replace(tzinfo=None)), + }, + blocking=True, + ) + + async_fire_time_changed(hass, trigger_dt) + await hass.async_block_till_done() + + assert len(calls) == 2 + assert calls[1].data["some"] == f"time-{trigger_dt.day}-{trigger_dt.hour}" + + async def test_if_fires_using_multiple_at(hass, calls): """Test for firing at.""" now = dt_util.utcnow() time_that_will_not_match_right_away = now.replace( - year=now.year + 1, hour=4, minute=59, second=0 - ) + hour=4, minute=59, second=0 + ) + timedelta(2) with patch( "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away @@ -98,7 +174,7 @@ async def test_if_fires_using_multiple_at(hass, calls): now = dt_util.utcnow() async_fire_time_changed( - hass, now.replace(year=now.year + 1, hour=5, minute=0, second=0) + hass, now.replace(hour=5, minute=0, second=0) + timedelta(2) ) await hass.async_block_till_done() @@ -106,7 +182,7 @@ async def test_if_fires_using_multiple_at(hass, calls): assert calls[0].data["some"] == "time - 5" async_fire_time_changed( - hass, now.replace(year=now.year + 1, hour=6, minute=0, second=0) + hass, now.replace(hour=6, minute=0, second=0) + timedelta(2) ) await hass.async_block_till_done() From 4fa346278c9d8c7c7bc6418f8df09ec3d4a14d50 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Aug 2020 22:54:41 +0200 Subject: [PATCH 109/862] Enable PAHO MQTT client logging (#38767) --- homeassistant/components/mqtt/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 73aee984ae3..702e8a139f4 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -666,6 +666,9 @@ class MQTT: else: self._mqttc = mqtt.Client(client_id, protocol=proto) + # Enable logging + self._mqttc.enable_logger() + username = self.conf.get(CONF_USERNAME) password = self.conf.get(CONF_PASSWORD) if username is not None: From cc4ebc925c1d75537f672bee61fdb6d54ab8fdb4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Aug 2020 22:57:50 +0200 Subject: [PATCH 110/862] Improve X-Forwarded-* request headers handling (#38696) Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof Co-authored-by: Pascal Vizeli --- homeassistant/components/auth/__init__.py | 9 +- homeassistant/components/auth/login_flow.py | 7 +- .../components/emulated_hue/__init__.py | 2 - .../components/emulated_hue/hue_api.py | 18 +- homeassistant/components/hassio/auth.py | 8 +- homeassistant/components/http/__init__.py | 12 +- homeassistant/components/http/auth.py | 4 +- homeassistant/components/http/ban.py | 8 +- homeassistant/components/http/const.py | 1 - homeassistant/components/http/forwarded.py | 174 +++++++ homeassistant/components/http/real_ip.py | 41 -- homeassistant/components/http/view.py | 7 +- .../components/telegram_bot/webhooks.py | 4 +- homeassistant/components/webhook/__init__.py | 3 +- tests/components/emulated_hue/test_hue_api.py | 2 +- tests/components/http/__init__.py | 6 +- tests/components/http/test_auth.py | 4 +- tests/components/http/test_forwarded.py | 487 ++++++++++++++++++ tests/components/http/test_real_ip.py | 101 ---- 19 files changed, 703 insertions(+), 195 deletions(-) create mode 100644 homeassistant/components/http/forwarded.py delete mode 100644 homeassistant/components/http/real_ip.py create mode 100644 tests/components/http/test_forwarded.py delete mode 100644 tests/components/http/test_real_ip.py diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 5b1ca46c41b..fba96b24084 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -127,7 +127,6 @@ from homeassistant.auth.models import ( User, ) from homeassistant.components import websocket_api -from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.auth import async_sign_path from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator @@ -252,14 +251,10 @@ class TokenView(HomeAssistantView): return await self._async_handle_revoke_token(hass, data) if grant_type == "authorization_code": - return await self._async_handle_auth_code( - hass, data, str(request[KEY_REAL_IP]) - ) + return await self._async_handle_auth_code(hass, data, request.remote) if grant_type == "refresh_token": - return await self._async_handle_refresh_token( - hass, data, str(request[KEY_REAL_IP]) - ) + return await self._async_handle_refresh_token(hass, data, request.remote) return self.json( {"error": "unsupported_grant_type"}, status_code=HTTP_BAD_REQUEST diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index c5d824ce617..31e3b7ea648 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -66,12 +66,13 @@ associate with an credential if "type" set to "link_user" in "version": 1 } """ +from ipaddress import ip_address + from aiohttp import web import voluptuous as vol import voluptuous_serialize from homeassistant import data_entry_flow -from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.ban import ( log_invalid_auth, process_success_login, @@ -183,7 +184,7 @@ class LoginFlowIndexView(HomeAssistantView): result = await self._flow_mgr.async_init( handler, context={ - "ip_address": request[KEY_REAL_IP], + "ip_address": ip_address(request.remote), "credential_only": data.get("type") == "link_user", }, ) @@ -231,7 +232,7 @@ class LoginFlowResourceView(HomeAssistantView): for flow in self._flow_mgr.async_progress(): if flow["flow_id"] == flow_id and flow["context"][ "ip_address" - ] != request.get(KEY_REAL_IP): + ] != ip_address(request.remote): return self.json_message("IP address changed", HTTP_BAD_REQUEST) result = await self._flow_mgr.async_configure(flow_id, data) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 9b4da5cfb21..8e855653642 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -5,7 +5,6 @@ from aiohttp import web import voluptuous as vol from homeassistant import util -from homeassistant.components.http import real_ip from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -101,7 +100,6 @@ async def async_setup(hass, yaml_config): app = web.Application() app["hass"] = hass - real_ip.setup_real_ip(app, False, []) # We misunderstood the startup signal. You're not allowed to change # anything during startup. Temp workaround. # pylint: disable=protected-access diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index b84e64e6cc6..239dd85d5a0 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,6 +1,7 @@ """Support for a Hue API to control Home Assistant.""" import asyncio import hashlib +from ipaddress import ip_address import logging import time @@ -34,7 +35,6 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, ) from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.humidifier.const import ( ATTR_HUMIDITY, SERVICE_SET_HUMIDITY, @@ -131,7 +131,7 @@ class HueUsernameView(HomeAssistantView): async def post(self, request): """Handle a POST request.""" - if not is_local(request[KEY_REAL_IP]): + if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) try: @@ -159,7 +159,7 @@ class HueAllGroupsStateView(HomeAssistantView): @core.callback def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" - if not is_local(request[KEY_REAL_IP]): + if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json({}) @@ -179,7 +179,7 @@ class HueGroupView(HomeAssistantView): @core.callback def put(self, request, username): """Process a request to make the Logitech Pop working.""" - if not is_local(request[KEY_REAL_IP]): + if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json( @@ -209,7 +209,7 @@ class HueAllLightsStateView(HomeAssistantView): @core.callback def get(self, request, username): """Process a request to get the list of available lights.""" - if not is_local(request[KEY_REAL_IP]): + if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json(create_list_of_entities(self.config, request)) @@ -229,7 +229,7 @@ class HueFullStateView(HomeAssistantView): @core.callback def get(self, request, username): """Process a request to get the list of available lights.""" - if not is_local(request[KEY_REAL_IP]): + if not is_local(ip_address(request.remote)): return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) if username != HUE_API_USERNAME: return self.json(UNAUTHORIZED_USER) @@ -256,7 +256,7 @@ class HueConfigView(HomeAssistantView): @core.callback def get(self, request, username): """Process a request to get the configuration.""" - if not is_local(request[KEY_REAL_IP]): + if not is_local(ip_address(request.remote)): return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) if username != HUE_API_USERNAME: return self.json(UNAUTHORIZED_USER) @@ -280,7 +280,7 @@ class HueOneLightStateView(HomeAssistantView): @core.callback def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" - if not is_local(request[KEY_REAL_IP]): + if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] @@ -321,7 +321,7 @@ class HueOneLightChangeView(HomeAssistantView): async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" - if not is_local(request[KEY_REAL_IP]): + if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) config = self.config diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 066219d77e8..48f0abd6617 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.const import KEY_HASS_USER, KEY_REAL_IP +from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import HTTP_OK from homeassistant.core import callback @@ -63,8 +63,10 @@ class HassIOBaseAuth(HomeAssistantView): """Check if this call is from Supervisor.""" # Check caller IP hassio_ip = os.environ["HASSIO"].split(":")[0] - if request[KEY_REAL_IP] != ip_address(hassio_ip): - _LOGGER.error("Invalid auth request from %s", request[KEY_REAL_IP]) + if ip_address(request.transport.get_extra_info("peername")[0]) != ip_address( + hassio_ip + ): + _LOGGER.error("Invalid auth request from %s", request.remote) raise HTTPUnauthorized() # Check caller token diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 36e44508928..cb0ecec8a2b 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -26,9 +26,9 @@ from homeassistant.util import ssl as ssl_util from .auth import setup_auth from .ban import setup_bans -from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER, KEY_REAL_IP # noqa: F401 +from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER # noqa: F401 from .cors import setup_cors -from .real_ip import setup_real_ip +from .forwarded import async_setup_forwarded from .request_context import setup_request_context from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa: F401 @@ -296,9 +296,13 @@ class HomeAssistantHTTP: ) app[KEY_HASS] = hass - # This order matters + # Order matters, forwarded middleware needs to go first. + # Only register middleware if `use_x_forwarded_for` is enabled + # and trusted proxies are provided + if use_x_forwarded_for and trusted_proxies: + async_setup_forwarded(app, trusted_proxies) + setup_request_context(app, current_request) - setup_real_ip(app, use_x_forwarded_for, trusted_proxies) if is_ban_enabled: setup_bans(hass, app, login_threshold) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 18d8ce72d91..f9e6df94489 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -9,7 +9,7 @@ import jwt from homeassistant.core import callback from homeassistant.util import dt as dt_util -from .const import KEY_AUTHENTICATED, KEY_HASS_USER, KEY_REAL_IP +from .const import KEY_AUTHENTICATED, KEY_HASS_USER # mypy: allow-untyped-defs, no-check-untyped-defs @@ -118,7 +118,7 @@ def setup_auth(hass, app): if authenticated: _LOGGER.debug( "Authenticated %s for %s using %s", - request[KEY_REAL_IP], + request.remote, request.path, auth_type, ) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 8b8d2bc5671..5a5b08a05c8 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -16,8 +16,6 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.util.yaml import dump -from .const import KEY_REAL_IP - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -61,7 +59,7 @@ async def ban_middleware(request, handler): return await handler(request) # Verify if IP is not banned - ip_address_ = request[KEY_REAL_IP] + ip_address_ = ip_address(request.remote) is_banned = any( ip_ban.ip_address == ip_address_ for ip_ban in request.app[KEY_BANNED_IPS] ) @@ -95,7 +93,7 @@ async def process_wrong_login(request): Increase failed login attempts counter for remote IP address. Add ip ban entry if failed login attempts exceeds threshold. """ - remote_addr = request[KEY_REAL_IP] + remote_addr = ip_address(request.remote) msg = f"Login attempt or request with invalid authentication from {remote_addr}" _LOGGER.warning(msg) @@ -144,7 +142,7 @@ async def process_success_login(request): No release IP address from banned list function, it can only be done by manual modify ip bans config file. """ - remote_addr = request[KEY_REAL_IP] + remote_addr = ip_address(request.remote) # Check if ban middleware is loaded if KEY_BANNED_IPS not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py index 9392e790d62..ebbc6cb9b81 100644 --- a/homeassistant/components/http/const.py +++ b/homeassistant/components/http/const.py @@ -2,4 +2,3 @@ KEY_AUTHENTICATED = "ha_authenticated" KEY_HASS = "hass" KEY_HASS_USER = "hass_user" -KEY_REAL_IP = "ha_real_ip" diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py new file mode 100644 index 00000000000..4d9ec69a018 --- /dev/null +++ b/homeassistant/components/http/forwarded.py @@ -0,0 +1,174 @@ +"""Middleware to handle forwarded data by a reverse proxy.""" +from ipaddress import ip_address +import logging + +from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO +from aiohttp.web import HTTPBadRequest, middleware + +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +# mypy: allow-untyped-defs + + +@callback +def async_setup_forwarded(app, trusted_proxies): + """Create forwarded middleware for the app. + + Process IP addresses, proto and host information in the forwarded for headers. + + `X-Forwarded-For: , , ` + e.g., `X-Forwarded-For: 203.0.113.195, 70.41.3.18, 150.172.238.178` + + We go through the list from the right side, and skip all entries that are in our + trusted proxies list. The first non-trusted IP is used as the client IP. If all + items in the X-Forwarded-For are trusted, including the most left item (client), + the most left item is used. In the latter case, the client connection originated + from an IP that is also listed as a trusted proxy IP or network. + + `X-Forwarded-Proto: , , ` + e.g., `X-Forwarded-Proto: https, http, http` + OR `X-Forwarded-Proto: https` (one entry, even with multiple proxies) + + The X-Forwarded-Proto is determined based on the corresponding entry of the + X-Forwarded-For header that is used/chosen as the client IP. However, + some proxies, for example, Kubernetes NGINX ingress, only retain one element + in the X-Forwarded-Proto header. In that case, we'll just use what we have. + + `X-Forwarded-Host: ` + e.g., `X-Forwarded-Host: example.com` + + If the previous headers are processed successfully, and the X-Forwarded-Host is + present, it will be used. + + Additionally: + - If no X-Forwarded-For header is found, the processing of all headers is skipped. + - Log a warning when untrusted connected peer provides X-Forwarded-For headers. + - If multiple instances of X-Forwarded-For, X-Forwarded-Proto or + X-Forwarded-Host are found, an HTTP 400 status code is thrown. + - If malformed or invalid (IP) data in X-Forwarded-For header is found, + an HTTP 400 status code is thrown. + - The connected client peer on the socket of the incoming connection, + must be trusted for any processing to take place. + - If the number of elements in X-Forwarded-Proto does not equal 1 or + is equal to the number of elements in X-Forwarded-For, an HTTP 400 + status code is thrown. + - If an empty X-Forwarded-Host is provided, an HTTP 400 status code is thrown. + - If an empty X-Forwarded-Proto is provided, or an empty element in the list, + an HTTP 400 status code is thrown. + """ + + @middleware + async def forwarded_middleware(request, handler): + """Process forwarded data by a reverse proxy.""" + overrides = {} + + # Handle X-Forwarded-For + forwarded_for_headers = request.headers.getall(X_FORWARDED_FOR, []) + if not forwarded_for_headers: + # No forwarding headers, continue as normal + return await handler(request) + + # Ensure the IP of the connected peer is trusted + connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) + if not any(connected_ip in trusted_proxy for trusted_proxy in trusted_proxies): + _LOGGER.warning( + "Received X-Forwarded-For header from untrusted proxy %s, headers not processed", + connected_ip, + ) + # Not trusted, continue as normal + return await handler(request) + + # Multiple X-Forwarded-For headers + if len(forwarded_for_headers) > 1: + _LOGGER.error( + "Too many headers for X-Forwarded-For: %s", forwarded_for_headers + ) + raise HTTPBadRequest + + # Process X-Forwarded-For from the right side (by reversing the list) + forwarded_for_split = list(reversed(forwarded_for_headers[0].split(","))) + try: + forwarded_for = [ip_address(addr.strip()) for addr in forwarded_for_split] + except ValueError: + _LOGGER.error( + "Invalid IP address in X-Forwarded-For: %s", forwarded_for_headers[0] + ) + raise HTTPBadRequest + + # Find the last trusted index in the X-Forwarded-For list + forwarded_for_index = 0 + for forwarded_ip in forwarded_for: + if any(forwarded_ip in trusted_proxy for trusted_proxy in trusted_proxies): + forwarded_for_index += 1 + continue + overrides["remote"] = str(forwarded_ip) + break + else: + # If all the IP addresses are from trusted networks, take the left-most. + forwarded_for_index = -1 + overrides["remote"] = str(forwarded_for[-1]) + + # Handle X-Forwarded-Proto + forwarded_proto_headers = request.headers.getall(X_FORWARDED_PROTO, []) + if forwarded_proto_headers: + if len(forwarded_proto_headers) > 1: + _LOGGER.error( + "Too many headers for X-Forward-Proto: %s", forwarded_proto_headers + ) + raise HTTPBadRequest + + forwarded_proto_split = list( + reversed(forwarded_proto_headers[0].split(",")) + ) + forwarded_proto = [proto.strip() for proto in forwarded_proto_split] + + # Catch empty values + if "" in forwarded_proto: + _LOGGER.error( + "Empty item received in X-Forward-Proto header: %s", + forwarded_proto_headers[0], + ) + raise HTTPBadRequest + + # The X-Forwarded-Proto contains either one element, or the equals number + # of elements as X-Forwarded-For + if len(forwarded_proto) not in (1, len(forwarded_for)): + _LOGGER.error( + "Incorrect number of elements in X-Forward-Proto. Expected 1 or %d, got %d: %s", + len(forwarded_for), + len(forwarded_proto), + forwarded_proto_headers[0], + ) + raise HTTPBadRequest + + # Ideally this should take the scheme corresponding to the entry + # in X-Forwarded-For that was chosen, but some proxies only retain + # one element. In that case, use what we have. + overrides["scheme"] = forwarded_proto[-1] + if len(forwarded_proto) != 1: + overrides["scheme"] = forwarded_proto[forwarded_for_index] + + # Handle X-Forwarded-Host + forwarded_host_headers = request.headers.getall(X_FORWARDED_HOST, []) + if forwarded_host_headers: + # Multiple X-Forwarded-Host headers + if len(forwarded_host_headers) > 1: + _LOGGER.error( + "Too many headers for X-Forwarded-Host: %s", forwarded_host_headers + ) + raise HTTPBadRequest + + forwarded_host = forwarded_host_headers[0].strip() + if not forwarded_host: + _LOGGER.error("Empty value received in X-Forward-Host header") + raise HTTPBadRequest + + overrides["host"] = forwarded_host + + # Done, create a new request based on gathered data. + request = request.clone(**overrides) + return await handler(request) + + app.middlewares.append(forwarded_middleware) diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py deleted file mode 100644 index f2334ce0a2f..00000000000 --- a/homeassistant/components/http/real_ip.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Middleware to fetch real IP.""" -from ipaddress import ip_address - -from aiohttp.hdrs import X_FORWARDED_FOR -from aiohttp.web import middleware - -from homeassistant.core import callback - -from .const import KEY_REAL_IP - -# mypy: allow-untyped-defs - - -@callback -def setup_real_ip(app, use_x_forwarded_for, trusted_proxies): - """Create IP Ban middleware for the app.""" - - @middleware - async def real_ip_middleware(request, handler): - """Real IP middleware.""" - connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) - request[KEY_REAL_IP] = connected_ip - - # Only use the XFF header if enabled, present, and from a trusted proxy - try: - if ( - use_x_forwarded_for - and X_FORWARDED_FOR in request.headers - and any( - connected_ip in trusted_proxy for trusted_proxy in trusted_proxies - ) - ): - request[KEY_REAL_IP] = ip_address( - request.headers.get(X_FORWARDED_FOR).split(", ")[-1] - ) - except ValueError: - pass - - return await handler(request) - - app.middlewares.append(real_ip_middleware) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index eb6c757384e..7c8e9281e42 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -18,7 +18,7 @@ from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK, HTTP_SERVICE_UNAVAIL from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder -from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP +from .const import KEY_AUTHENTICATED, KEY_HASS _LOGGER = logging.getLogger(__name__) @@ -116,10 +116,7 @@ def request_handler_factory(view: HomeAssistantView, handler: Callable) -> Calla raise HTTPUnauthorized() _LOGGER.debug( - "Serving %s to %s (auth: %s)", - request.path, - request.get(KEY_REAL_IP), - authenticated, + "Serving %s to %s (auth: %s)", request.path, request.remote, authenticated, ) try: diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 7c8f976a049..4d5f41066e2 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -1,11 +1,11 @@ """Support for Telegram bots using webhooks.""" import datetime as dt +from ipaddress import ip_address import logging from telegram.error import TimedOut from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST, @@ -96,7 +96,7 @@ class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): async def post(self, request): """Accept the POST from telegram.""" - real_ip = request[KEY_REAL_IP] + real_ip = ip_address(request.remote) if not any(real_ip in net for net in self.trusted_networks): _LOGGER.warning("Access denied from %s", real_ip) return self.json_message("Access denied", HTTP_UNAUTHORIZED) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 99226cabaa7..9c6dfe45e74 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -6,7 +6,6 @@ from aiohttp.web import Request, Response import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.view import HomeAssistantView from homeassistant.const import HTTP_OK from homeassistant.core import callback @@ -80,7 +79,7 @@ async def async_handle_webhook(hass, webhook_id, request): if isinstance(request, MockRequest): received_from = request.mock_source else: - received_from = request[KEY_REAL_IP] + received_from = request.remote _LOGGER.warning( "Received message for unregistered webhook %s from %s", diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 510aa0ef8ee..9c6fb9d37a8 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1157,7 +1157,7 @@ async def test_external_ip_blocked(hue_client): postUrls = ["/api"] putUrls = ["/api/username/lights/light.ceiling_lights/state"] with patch( - "homeassistant.components.http.real_ip.ip_address", + "homeassistant.components.emulated_hue.hue_api.ip_address", return_value=ip_address("45.45.45.45"), ): for getUrl in getUrls: diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py index e96f4a7fcf2..238f5c7050a 100644 --- a/tests/components/http/__init__.py +++ b/tests/components/http/__init__.py @@ -1,10 +1,6 @@ """Tests for the HTTP component.""" -from ipaddress import ip_address - from aiohttp import web -from homeassistant.components.http.const import KEY_REAL_IP - # Relic from the past. Kept here so we can run negative tests. HTTP_HEADER_HA_AUTH = "X-HA-access" @@ -25,7 +21,7 @@ def mock_real_ip(app): """Mock Real IP middleware.""" nonlocal ip_to_mock - request[KEY_REAL_IP] = ip_address(ip_to_mock) + request = request.clone(remote=ip_to_mock) return await handler(request) diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 9282bf4587b..e3274ddfa7d 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -9,7 +9,7 @@ import pytest from homeassistant.auth.providers import trusted_networks from homeassistant.components.http.auth import async_sign_path, setup_auth from homeassistant.components.http.const import KEY_AUTHENTICATED -from homeassistant.components.http.real_ip import setup_real_ip +from homeassistant.components.http.forwarded import async_setup_forwarded from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH, mock_real_ip @@ -54,7 +54,7 @@ def app(hass): app = web.Application() app["hass"] = hass app.router.add_get("/", mock_handler) - setup_real_ip(app, False, []) + async_setup_forwarded(app, []) return app diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py new file mode 100644 index 00000000000..45853687f13 --- /dev/null +++ b/tests/components/http/test_forwarded.py @@ -0,0 +1,487 @@ +"""Test real forwarded middleware.""" +from ipaddress import ip_network + +from aiohttp import web +from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO +import pytest + +from homeassistant.components.http.forwarded import async_setup_forwarded + + +async def mock_handler(request): + """Return the real IP as text.""" + return web.Response(text=request.remote) + + +async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): + """Test that we get the IP from the transport.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + + async_setup_forwarded(app, []) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) + + assert resp.status == 200 + assert ( + "Received X-Forwarded-For header from untrusted proxy 127.0.0.1, headers not processed" + in caplog.text + ) + + +@pytest.mark.parametrize( + "trusted_proxies,x_forwarded_for,remote", + [ + ( + ["127.0.0.0/24", "1.1.1.1", "10.10.10.0/24"], + "10.10.10.10, 1.1.1.1", + "10.10.10.10", + ), + (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), + (["127.0.0.0/24", "1.1.1.1"], "123.123.123.123,2.2.2.2,1.1.1.1", "2.2.2.2"), + (["127.0.0.0/24"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "1.1.1.1"), + (["127.0.0.0/24"], "127.0.0.1", "127.0.0.1"), + (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 1.1.1.1", "123.123.123.123"), + (["127.0.0.1", "1.1.1.1"], "123.123.123.123, 2.2.2.2, 1.1.1.1", "2.2.2.2"), + (["127.0.0.1"], "255.255.255.255", "255.255.255.255"), + ], +) +async def test_x_forwarded_for_with_trusted_proxy( + trusted_proxies, x_forwarded_for, remote, aiohttp_client +): + """Test that we get the IP from the forwarded for header.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == remote + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + async_setup_forwarded( + app, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] + ) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: x_forwarded_for}) + + assert resp.status == 200 + + +async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): + """Test that we get the IP from transport with untrusted proxy.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + async_setup_forwarded(app, [ip_network("1.1.1.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) + + assert resp.status == 200 + + +async def test_x_forwarded_for_with_spoofed_header(aiohttp_client): + """Test that we get the IP from the transport with a spoofed header.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "255.255.255.255" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", headers={X_FORWARDED_FOR: "222.222.222.222, 255.255.255.255"} + ) + + assert resp.status == 200 + + +@pytest.mark.parametrize( + "x_forwarded_for", + [ + "This value is invalid", + "1.1.1.1, , 1.2.3.4", + "1.1.1.1,,1.2.3.4", + "1.1.1.1, batman, 1.2.3.4", + "192.168.0.0/24", + "192.168.0.0/24, 1.1.1.1", + ",", + "", + ], +) +async def test_x_forwarded_for_with_malformed_header( + x_forwarded_for, aiohttp_client, caplog +): + """Test that we get a HTTP 400 bad request with a malformed header.""" + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: x_forwarded_for}) + + assert resp.status == 400 + assert "Invalid IP address in X-Forwarded-For" in caplog.text + + +async def test_x_forwarded_for_with_multiple_headers(aiohttp_client, caplog): + """Test that we get a HTTP 400 bad request with multiple headers.""" + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get( + "/", + headers=[ + (X_FORWARDED_FOR, "222.222.222.222"), + (X_FORWARDED_FOR, "123.123.123.123"), + ], + ) + + assert resp.status == 400 + assert "Too many headers for X-Forwarded-For" in caplog.text + + +async def test_x_forwarded_proto_without_trusted_proxy(aiohttp_client): + """Test that proto header is ignored when untrusted.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + + async_setup_forwarded(app, []) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", headers={X_FORWARDED_FOR: "255.255.255.255", X_FORWARDED_PROTO: "https"} + ) + + assert resp.status == 200 + + +@pytest.mark.parametrize( + "x_forwarded_for,remote,x_forwarded_proto,secure", + [ + ("10.10.10.10, 127.0.0.1, 127.0.0.2", "10.10.10.10", "https, http, http", True), + ("10.10.10.10, 127.0.0.1, 127.0.0.2", "10.10.10.10", "https,http,http", True), + ("10.10.10.10, 127.0.0.1, 127.0.0.2", "10.10.10.10", "http", False), + ( + "10.10.10.10, 127.0.0.1, 127.0.0.2", + "10.10.10.10", + "http, https, https", + False, + ), + ("10.10.10.10, 127.0.0.1, 127.0.0.2", "10.10.10.10", "https", True), + ( + "255.255.255.255, 10.10.10.10, 127.0.0.1", + "10.10.10.10", + "http, https, http", + True, + ), + ( + "255.255.255.255, 10.10.10.10, 127.0.0.1", + "10.10.10.10", + "https, http, https", + False, + ), + ("255.255.255.255, 10.10.10.10, 127.0.0.1", "10.10.10.10", "https", True), + ], +) +async def test_x_forwarded_proto_with_trusted_proxy( + x_forwarded_for, remote, x_forwarded_proto, secure, aiohttp_client +): + """Test that we get the proto header if proxy is trusted.""" + + async def handler(request): + assert request.remote == remote + assert request.scheme == ("https" if secure else "http") + assert request.secure == secure + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", + headers={ + X_FORWARDED_FOR: x_forwarded_for, + X_FORWARDED_PROTO: x_forwarded_proto, + }, + ) + + assert resp.status == 200 + + +async def test_x_forwarded_proto_with_trusted_proxy_multiple_for(aiohttp_client): + """Test that we get the proto with 1 element in the proto, multiple in the for.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "https" + assert request.secure + assert request.remote == "255.255.255.255" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", + headers={ + X_FORWARDED_FOR: "255.255.255.255, 127.0.0.1, 127.0.0.2", + X_FORWARDED_PROTO: "https", + }, + ) + + assert resp.status == 200 + + +async def test_x_forwarded_proto_not_processed_without_for(aiohttp_client): + """Test that proto header isn't processed without a for header.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_PROTO: "https"}) + + assert resp.status == 200 + + +async def test_x_forwarded_proto_with_multiple_headers(aiohttp_client, caplog): + """Test that we get a HTTP 400 bad request with multiple headers.""" + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", + headers=[ + (X_FORWARDED_FOR, "222.222.222.222"), + (X_FORWARDED_PROTO, "https"), + (X_FORWARDED_PROTO, "http"), + ], + ) + + assert resp.status == 400 + assert "Too many headers for X-Forward-Proto" in caplog.text + + +@pytest.mark.parametrize( + "x_forwarded_proto", ["", ",", "https, , https", "https, https, "], +) +async def test_x_forwarded_proto_empty_element( + x_forwarded_proto, aiohttp_client, caplog +): + """Test that we get a HTTP 400 bad request with empty proto.""" + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", headers={X_FORWARDED_FOR: "1.1.1.1", X_FORWARDED_PROTO: x_forwarded_proto}, + ) + + assert resp.status == 400 + assert "Empty item received in X-Forward-Proto header" in caplog.text + + +@pytest.mark.parametrize( + "x_forwarded_for,x_forwarded_proto,expected,got", + [ + ("1.1.1.1, 2.2.2.2", "https, https, https", 2, 3), + ("1.1.1.1, 2.2.2.2, 3.3.3.3, 4.4.4.4", "https, https, https", 4, 3), + ], +) +async def test_x_forwarded_proto_incorrect_number_of_elements( + x_forwarded_for, x_forwarded_proto, expected, got, aiohttp_client, caplog +): + """Test that we get a HTTP 400 bad request with incorrect number of elements.""" + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", + headers={ + X_FORWARDED_FOR: x_forwarded_for, + X_FORWARDED_PROTO: x_forwarded_proto, + }, + ) + + assert resp.status == 400 + assert ( + f"Incorrect number of elements in X-Forward-Proto. Expected 1 or {expected}, got {got}" + in caplog.text + ) + + +async def test_x_forwarded_host_without_trusted_proxy(aiohttp_client): + """Test that host header is ignored when untrusted.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + + async_setup_forwarded(app, []) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", + headers={X_FORWARDED_FOR: "255.255.255.255", X_FORWARDED_HOST: "example.com"}, + ) + + assert resp.status == 200 + + +async def test_x_forwarded_host_with_trusted_proxy(aiohttp_client): + """Test that we get the host header if proxy is trusted.""" + + async def handler(request): + assert request.host == "example.com" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "255.255.255.255" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", + headers={X_FORWARDED_FOR: "255.255.255.255", X_FORWARDED_HOST: "example.com"}, + ) + + assert resp.status == 200 + + +async def test_x_forwarded_host_not_processed_without_for(aiohttp_client): + """Test that host header isn't processed without a for header.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_HOST: "example.com"}) + + assert resp.status == 200 + + +async def test_x_forwarded_host_with_multiple_headers(aiohttp_client, caplog): + """Test that we get a HTTP 400 bad request with multiple headers.""" + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", + headers=[ + (X_FORWARDED_FOR, "222.222.222.222"), + (X_FORWARDED_HOST, "example.com"), + (X_FORWARDED_HOST, "example.spoof"), + ], + ) + + assert resp.status == 400 + assert "Too many headers for X-Forwarded-Host" in caplog.text + + +async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog): + """Test that we get a HTTP 400 bad request with empty host value.""" + app = web.Application() + app.router.add_get("/", mock_handler) + async_setup_forwarded(app, [ip_network("127.0.0.1")]) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get( + "/", headers={X_FORWARDED_FOR: "222.222.222.222", X_FORWARDED_HOST: ""} + ) + + assert resp.status == 400 + assert "Empty value received in X-Forward-Host header" in caplog.text diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py deleted file mode 100644 index 2cb74df3176..00000000000 --- a/tests/components/http/test_real_ip.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Test real IP middleware.""" -from ipaddress import ip_network - -from aiohttp import web -from aiohttp.hdrs import X_FORWARDED_FOR - -from homeassistant.components.http.const import KEY_REAL_IP -from homeassistant.components.http.real_ip import setup_real_ip - - -async def mock_handler(request): - """Return the real IP as text.""" - return web.Response(text=str(request[KEY_REAL_IP])) - - -async def test_ignore_x_forwarded_for(aiohttp_client): - """Test that we get the IP from the transport.""" - app = web.Application() - app.router.add_get("/", mock_handler) - setup_real_ip(app, False, []) - - mock_api_client = await aiohttp_client(app) - - resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) - assert resp.status == 200 - text = await resp.text() - assert text != "255.255.255.255" - - -async def test_use_x_forwarded_for_without_trusted_proxy(aiohttp_client): - """Test that we get the IP from the transport.""" - app = web.Application() - app.router.add_get("/", mock_handler) - setup_real_ip(app, True, []) - - mock_api_client = await aiohttp_client(app) - - resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) - assert resp.status == 200 - text = await resp.text() - assert text != "255.255.255.255" - - -async def test_use_x_forwarded_for_with_trusted_proxy(aiohttp_client): - """Test that we get the IP from the transport.""" - app = web.Application() - app.router.add_get("/", mock_handler) - setup_real_ip(app, True, [ip_network("127.0.0.1")]) - - mock_api_client = await aiohttp_client(app) - - resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) - assert resp.status == 200 - text = await resp.text() - assert text == "255.255.255.255" - - -async def test_use_x_forwarded_for_with_untrusted_proxy(aiohttp_client): - """Test that we get the IP from the transport.""" - app = web.Application() - app.router.add_get("/", mock_handler) - setup_real_ip(app, True, [ip_network("1.1.1.1")]) - - mock_api_client = await aiohttp_client(app) - - resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) - assert resp.status == 200 - text = await resp.text() - assert text != "255.255.255.255" - - -async def test_use_x_forwarded_for_with_spoofed_header(aiohttp_client): - """Test that we get the IP from the transport.""" - app = web.Application() - app.router.add_get("/", mock_handler) - setup_real_ip(app, True, [ip_network("127.0.0.1")]) - - mock_api_client = await aiohttp_client(app) - - resp = await mock_api_client.get( - "/", headers={X_FORWARDED_FOR: "222.222.222.222, 255.255.255.255"} - ) - assert resp.status == 200 - text = await resp.text() - assert text == "255.255.255.255" - - -async def test_use_x_forwarded_for_with_nonsense_header(aiohttp_client): - """Test that we get the IP from the transport.""" - app = web.Application() - app.router.add_get("/", mock_handler) - setup_real_ip(app, True, [ip_network("127.0.0.1")]) - - mock_api_client = await aiohttp_client(app) - - resp = await mock_api_client.get( - "/", headers={X_FORWARDED_FOR: "This value is invalid"} - ) - assert resp.status == 200 - text = await resp.text() - assert text == "127.0.0.1" From 61a911af41f9fe954e653abdb51357bbdfaa356c Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 11 Aug 2020 13:59:26 -0700 Subject: [PATCH 111/862] Adapt the ONVIF Renewal termination_time for Amcrest cameras (#37750) --- homeassistant/components/onvif/event.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 60a92e56ac0..2516a4805e2 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -108,7 +108,9 @@ class EventManager: return termination_time = ( - (dt_util.utcnow() + dt.timedelta(days=1)).replace(microsecond=0).isoformat() + (dt_util.utcnow() + dt.timedelta(days=1)) + .isoformat(timespec="seconds") + .replace("+00:00", "Z") ) await self._subscription.Renew(termination_time) From d0a59e28acfee4a188fe892446c26117276fb798 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Aug 2020 23:03:56 +0200 Subject: [PATCH 112/862] Revert "Add energy device class to Toon sensors" (#38768) This reverts commit dd86de3255485c7904c7d82ad467b6b4fb0e5c18. --- homeassistant/components/toon/const.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index a5f57f95cd4..c814134f767 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -5,11 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PROBLEM, ) -from homeassistant.components.sensor import ( - DEVICE_CLASS_ENERGY, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, -) +from homeassistant.components.sensor import DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, @@ -196,7 +192,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_average", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -214,7 +210,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: True, }, @@ -223,7 +219,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -232,7 +228,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -250,7 +246,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_produced_high", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -259,7 +255,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "meter_produced_low", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, @@ -295,7 +291,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_produced_solar", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:solar-power", ATTR_DEFAULT_ENABLED: True, }, @@ -304,7 +300,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_to_grid_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:solar-power", ATTR_DEFAULT_ENABLED: False, }, @@ -313,7 +309,7 @@ SENSOR_ENTITIES = { ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "day_from_grid_usage", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:power-plug", ATTR_DEFAULT_ENABLED: False, }, From 5355fcaba8efc21ab342a5229eb30dd6d71f3285 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 12 Aug 2020 05:12:41 +0800 Subject: [PATCH 113/862] Add H.265 support to stream component (#38125) * Add H.265 support to stream component * Change find_box to generator * Move fmp4 utilities to fmp4utils.py * Add minimum segments and segment durations * Remove MIN_SEGMENTS * Fix when container_options is None * Fix missing num_segments and update tests * Remove unnecessary mock attribute * Fix Segment construction in test_recorder_save * fix recorder with lookback Co-authored-by: Jason Hunter --- homeassistant/components/stream/__init__.py | 3 +- homeassistant/components/stream/const.py | 3 ++ homeassistant/components/stream/core.py | 22 ++++---- homeassistant/components/stream/fmp4utils.py | 50 +++++++++++++++++ homeassistant/components/stream/hls.py | 56 +++++++++++++++----- homeassistant/components/stream/recorder.py | 18 +++---- homeassistant/components/stream/worker.py | 44 +++++++++++---- tests/components/stream/common.py | 2 +- tests/components/stream/test_hls.py | 16 ++++-- tests/components/stream/test_init.py | 1 - tests/components/stream/test_recorder.py | 5 +- 11 files changed, 169 insertions(+), 51 deletions(-) create mode 100644 homeassistant/components/stream/fmp4utils.py diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index a84f37ee126..aeab212b78c 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -18,6 +18,7 @@ from .const import ( CONF_LOOKBACK, CONF_STREAM_SOURCE, DOMAIN, + MAX_SEGMENTS, SERVICE_RECORD, ) from .core import PROVIDERS @@ -225,7 +226,7 @@ async def async_handle_record_service(hass, call): # Take advantage of lookback hls = stream.outputs.get("hls") if lookback > 0 and hls: - num_segments = min(int(lookback // hls.target_duration), hls.num_segments) + num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) # Wait for latest segment, then add the lookback await hls.recv() recorder.prepend(list(hls.get_segment())[-num_segments:]) diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 2f50ff26226..7bc900c25e6 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -16,3 +16,6 @@ OUTPUT_FORMATS = ["hls"] FORMAT_CONTENT_TYPE = {"hls": "application/vnd.apple.mpegurl"} AUDIO_SAMPLE_RATE = 44100 + +MAX_SEGMENTS = 3 # Max number of segments to keep around +MIN_SEGMENT_DURATION = 1.5 # Each segment is at least this many seconds diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 715ae47e133..06e1e659202 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -12,7 +12,7 @@ from homeassistant.core import callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry -from .const import ATTR_STREAMS, DOMAIN +from .const import ATTR_STREAMS, DOMAIN, MAX_SEGMENTS PROVIDERS = Registry() @@ -34,13 +34,12 @@ class Segment: sequence: int = attr.ib() segment: io.BytesIO = attr.ib() duration: float = attr.ib() + start_pts: tuple = attr.ib() class StreamOutput: """Represents a stream output.""" - num_segments = 3 - def __init__(self, stream, timeout: int = 300) -> None: """Initialize a stream output.""" self.idle = False @@ -48,7 +47,7 @@ class StreamOutput: self._stream = stream self._cursor = None self._event = asyncio.Event() - self._segments = deque(maxlen=self.num_segments) + self._segments = deque(maxlen=MAX_SEGMENTS) self._unsub = None @property @@ -67,8 +66,13 @@ class StreamOutput: return None @property - def video_codec(self) -> str: - """Return desired video codec.""" + def video_codecs(self) -> tuple: + """Return desired video codecs.""" + return None + + @property + def container_options(self) -> dict: + """Return container options.""" return None @property @@ -78,12 +82,12 @@ class StreamOutput: @property def target_duration(self) -> int: - """Return the average duration of the segments in seconds.""" + """Return the max duration of any given segment in seconds.""" segment_length = len(self._segments) if not segment_length: return 0 durations = [s.duration for s in self._segments] - return round(sum(durations) // segment_length) or 1 + return round(max(durations)) or 1 def get_segment(self, sequence: int = None) -> Any: """Retrieve a specific segment, or the whole list.""" @@ -147,7 +151,7 @@ class StreamOutput: def cleanup(self): """Handle cleanup.""" - self._segments = deque(maxlen=self.num_segments) + self._segments = deque(maxlen=MAX_SEGMENTS) self._stream.remove_provider(self) diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py new file mode 100644 index 00000000000..025e576bc06 --- /dev/null +++ b/homeassistant/components/stream/fmp4utils.py @@ -0,0 +1,50 @@ +"""Utilities to help convert mp4s to fmp4s.""" +import io + + +def find_box(segment: io.BytesIO, target_type: bytes, box_start: int = 0) -> int: + """Find location of first box (or sub_box if box_start provided) of given type.""" + if box_start == 0: + box_end = len(segment.getbuffer()) + index = 0 + else: + segment.seek(box_start) + box_end = box_start + int.from_bytes(segment.read(4), byteorder="big") + index = box_start + 8 + while 1: + if index > box_end - 8: # End of box, not found + break + segment.seek(index) + box_header = segment.read(8) + if box_header[4:8] == target_type: + yield index + segment.seek(index) + index += int.from_bytes(box_header[0:4], byteorder="big") + + +def get_init(segment: io.BytesIO) -> bytes: + """Get init section from fragmented mp4.""" + moof_location = next(find_box(segment, b"moof")) + segment.seek(0) + return segment.read(moof_location) + + +def get_m4s(segment: io.BytesIO, start_pts: tuple, sequence: int) -> bytes: + """Get m4s section from fragmented mp4.""" + moof_location = next(find_box(segment, b"moof")) + mfra_location = next(find_box(segment, b"mfra")) + # adjust mfhd sequence number in moof + view = segment.getbuffer() + view[moof_location + 20 : moof_location + 24] = sequence.to_bytes(4, "big") + # adjust tfdt in video traf + traf_finder = find_box(segment, b"traf", moof_location) + traf_location = next(traf_finder) + tfdt_location = next(find_box(segment, b"tfdt", traf_location)) + view[tfdt_location + 12 : tfdt_location + 20] = start_pts[0].to_bytes(8, "big") + # adjust tfdt in audio traf + traf_location = next(traf_finder) + tfdt_location = next(find_box(segment, b"tfdt", traf_location)) + view[tfdt_location + 12 : tfdt_location + 20] = start_pts[1].to_bytes(8, "big") + # done adjusting + segment.seek(moof_location) + return segment.read(mfra_location - moof_location) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 2cd98c0a00f..66cf3583b60 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -6,6 +6,7 @@ from homeassistant.util.dt import utcnow from .const import FORMAT_CONTENT_TYPE from .core import PROVIDERS, StreamOutput, StreamView +from .fmp4utils import get_init, get_m4s @callback @@ -13,6 +14,7 @@ def async_setup_hls(hass): """Set up api endpoints.""" hass.http.register_view(HlsPlaylistView()) hass.http.register_view(HlsSegmentView()) + hass.http.register_view(HlsInitView()) return "/api/hls/{}/playlist.m3u8" @@ -37,21 +39,41 @@ class HlsPlaylistView(StreamView): ) -class HlsSegmentView(StreamView): - """Stream view to serve a MPEG2TS segment.""" +class HlsInitView(StreamView): + """Stream view to serve HLS init.mp4.""" - url = r"/api/hls/{token:[a-f0-9]+}/segment/{sequence:\d+}.ts" + url = r"/api/hls/{token:[a-f0-9]+}/init.mp4" + name = "api:stream:hls:init" + cors_allowed = True + + async def handle(self, request, stream, sequence): + """Return init.mp4.""" + track = stream.add_provider("hls") + segments = track.get_segment() + if not segments: + return web.HTTPNotFound() + headers = {"Content-Type": "video/mp4"} + return web.Response(body=get_init(segments[0].segment), headers=headers) + + +class HlsSegmentView(StreamView): + """Stream view to serve a HLS fmp4 segment.""" + + url = r"/api/hls/{token:[a-f0-9]+}/segment/{sequence:\d+}.m4s" name = "api:stream:hls:segment" cors_allowed = True async def handle(self, request, stream, sequence): - """Return mpegts segment.""" + """Return fmp4 segment.""" track = stream.add_provider("hls") segment = track.get_segment(int(sequence)) if not segment: return web.HTTPNotFound() - headers = {"Content-Type": "video/mp2t"} - return web.Response(body=segment.segment.getvalue(), headers=headers) + headers = {"Content-Type": "video/iso.segment"} + return web.Response( + body=get_m4s(segment.segment, segment.start_pts, int(sequence)), + headers=headers, + ) class M3U8Renderer: @@ -64,7 +86,12 @@ class M3U8Renderer: @staticmethod def render_preamble(track): """Render preamble.""" - return ["#EXT-X-VERSION:3", f"#EXT-X-TARGETDURATION:{track.target_duration}"] + return [ + "#EXT-X-VERSION:7", + f"#EXT-X-TARGETDURATION:{track.target_duration}", + '#EXT-X-MAP:URI="init.mp4"', + "#EXT-X-INDEPENDENT-SEGMENTS", + ] @staticmethod def render_playlist(track, start_time): @@ -81,7 +108,7 @@ class M3U8Renderer: playlist.extend( [ "#EXTINF:{:.04f},".format(float(segment.duration)), - f"./segment/{segment.sequence}.ts", + f"./segment/{segment.sequence}.m4s", ] ) @@ -109,7 +136,7 @@ class HlsStreamOutput(StreamOutput): @property def format(self) -> str: """Return container format.""" - return "mpegts" + return "mp4" @property def audio_codec(self) -> str: @@ -117,6 +144,11 @@ class HlsStreamOutput(StreamOutput): return "aac" @property - def video_codec(self) -> str: - """Return desired video codec.""" - return "h264" + def video_codecs(self) -> tuple: + """Return desired video codecs.""" + return {"hevc", "h264"} + + @property + def container_options(self) -> dict: + """Return container options.""" + return {"movflags": "frag_custom+empty_moov+default_base_moof"} diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index c28c73c64ac..90d6cdccfc1 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -17,14 +17,14 @@ def async_setup_recorder(hass): def recorder_save_worker(file_out: str, segments: List[Segment]): """Handle saving stream.""" - first_pts = None + first_pts = segments[0].start_pts[0] output = av.open(file_out, "w") output_v = None for segment in segments: # Seek to beginning and open segment segment.segment.seek(0) - source = av.open(segment.segment, "r", format="mpegts") + source = av.open(segment.segment, "r", format="mp4") source_v = source.streams.video[0] # Add output streams @@ -36,9 +36,9 @@ def recorder_save_worker(file_out: str, segments: List[Segment]): # Remux video for packet in source.demux(source_v): if packet is not None and packet.dts is not None: - if first_pts is None: - first_pts = packet.pts - + if packet.pts < segment.start_pts[0]: + packet.pts += segment.start_pts[0] + packet.dts += segment.start_pts[0] packet.pts -= first_pts packet.dts -= first_pts packet.stream = output_v @@ -67,7 +67,7 @@ class RecorderOutput(StreamOutput): @property def format(self) -> str: """Return container format.""" - return "mpegts" + return "mp4" @property def audio_codec(self) -> str: @@ -75,9 +75,9 @@ class RecorderOutput(StreamOutput): return "aac" @property - def video_codec(self) -> str: - """Return desired video codec.""" - return "h264" + def video_codecs(self) -> tuple: + """Return desired video codecs.""" + return {"hevc", "h264"} def prepend(self, segments: List[Segment]) -> None: """Prepend segments to existing list.""" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 965f2611ed4..461d29de421 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -5,7 +5,7 @@ import logging import av -from .const import AUDIO_SAMPLE_RATE +from .const import AUDIO_SAMPLE_RATE, MIN_SEGMENT_DURATION from .core import Segment, StreamBuffer _LOGGER = logging.getLogger(__name__) @@ -29,7 +29,15 @@ def create_stream_buffer(stream_output, video_stream, audio_frame): a_packet = None segment = io.BytesIO() - output = av.open(segment, mode="w", format=stream_output.format) + output = av.open( + segment, + mode="w", + format=stream_output.format, + container_options={ + "video_track_timescale": str(int(1 / video_stream.time_base)), + **(stream_output.container_options or {}), + }, + ) vstream = output.add_stream(template=video_stream) # Check if audio is requested astream = None @@ -68,6 +76,9 @@ def stream_worker(hass, stream, quit_event): last_dts = None # Keep track of consecutive packets without a dts to detect end of stream. last_packet_was_without_dts = False + # The pts at the beginning of the segment + segment_start_v_pts = 0 + segment_start_a_pts = 0 while not quit_event.is_set(): try: @@ -99,13 +110,15 @@ def stream_worker(hass, stream, quit_event): packet.dts -= first_pts packet.pts -= first_pts - # Reset segment on every keyframe - if packet.is_keyframe: - # Calculate the segment duration by multiplying the presentation - # timestamp by the time base, which gets us total seconds. - # By then dividing by the sequence, we can calculate how long - # each segment is, assuming the stream starts from 0. - segment_duration = (packet.pts * packet.time_base) / sequence + # Reset segment on keyframe after we reach desired segment duration + if ( + packet.is_keyframe + and (packet.pts - segment_start_v_pts) * packet.time_base + >= MIN_SEGMENT_DURATION + ): + # Calculate the segment duration by multiplying the difference of the next and the current + # keyframe presentation timestamps by the time base, which gets us total seconds. + segment_duration = (packet.pts - segment_start_v_pts) * packet.time_base # Save segment to outputs for fmt, buffer in outputs.items(): buffer.output.close() @@ -113,17 +126,26 @@ def stream_worker(hass, stream, quit_event): if stream.outputs.get(fmt): hass.loop.call_soon_threadsafe( stream.outputs[fmt].put, - Segment(sequence, buffer.segment, segment_duration), + Segment( + sequence, + buffer.segment, + segment_duration, + (segment_start_v_pts, segment_start_a_pts), + ), ) # Clear outputs and increment sequence outputs = {} if not first_packet: sequence += 1 + segment_start_v_pts = packet.pts + segment_start_a_pts = int( + packet.pts * packet.time_base * AUDIO_SAMPLE_RATE + ) # Initialize outputs for stream_output in stream.outputs.values(): - if video_stream.name != stream_output.video_codec: + if video_stream.name not in stream_output.video_codecs: continue a_packet, buffer = create_stream_buffer( diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index 4c34ec0b341..354b94f7089 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -20,7 +20,7 @@ def generate_h264_video(): total_frames = duration * fps output = io.BytesIO() - output.name = "test.ts" + output.name = "test.mp4" container = av.open(output, mode="w") stream = container.add_stream("libx264", rate=fps) diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 3de50d3309c..b4c0b0e536f 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -38,6 +38,13 @@ async def test_hls_stream(hass, hass_client): playlist_response = await http_client.get(parsed_url.path) assert playlist_response.status == 200 + # Fetch init + playlist = await playlist_response.text() + playlist_url = "/".join(parsed_url.path.split("/")[:-1]) + init_url = playlist_url + "/init.mp4" + init_response = await http_client.get(init_url) + assert init_response.status == 200 + # Fetch segment playlist = await playlist_response.text() playlist_url = "/".join(parsed_url.path.split("/")[:-1]) @@ -99,15 +106,16 @@ async def test_stream_ended(hass): source = generate_h264_video() stream = preload_stream(hass, source) track = stream.add_provider("hls") - track.num_segments = 2 # Request stream request_stream(hass, source) # Run it dead - segments = 0 - while await track.recv() is not None: - segments += 1 + while True: + segment = await track.recv() + if segment is None: + break + segments = segment.sequence assert segments > 1 assert not track.get_segment() diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index dc7892e069a..505de7ca018 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -72,7 +72,6 @@ async def test_record_service_lookback(hass): ): # Setup stubs hls_mock = MagicMock() - hls_mock.num_segments = 3 hls_mock.target_duration = 2 hls_mock.recv = AsyncMock(return_value=None) stream_mock.return_value.outputs = {"hls": hls_mock} diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index fbbeaf0ff44..ff18cb66590 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -31,12 +31,11 @@ async def test_record_stream(hass, hass_client): recorder = stream.add_provider("recorder") stream.start() - segments = 0 while True: segment = await recorder.recv() if not segment: break - segments += 1 + segments = segment.sequence stream.stop() @@ -76,7 +75,7 @@ async def test_recorder_save(): output.name = "test.mp4" # Run - recorder_save_worker(output, [Segment(1, source, 4)]) + recorder_save_worker(output, [Segment(1, source, 4, (360000, 176400))]) # Assert assert output.getvalue() From 4d1ef02802cc9742ed93acc367977113ed1aeda2 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 11 Aug 2020 14:20:43 -0700 Subject: [PATCH 114/862] Stream clients operate on a copy of the intnernal self._outputs dict (#38766) --- homeassistant/components/stream/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index aeab212b78c..520e3047439 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -2,6 +2,7 @@ import logging import secrets import threading +from types import MappingProxyType import voluptuous as vol @@ -137,8 +138,10 @@ class Stream: @property def outputs(self): - """Return stream outputs.""" - return self._outputs + """Return a copy of the stream outputs.""" + # A copy is returned so the caller can iterate through the outputs + # without concern about self._outputs being modified from another thread. + return MappingProxyType(self._outputs.copy()) def add_provider(self, fmt): """Add provider output stream.""" From 49b375ff94cb4e3ea280a8095cefac4031eeaee7 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 11 Aug 2020 14:53:30 -0700 Subject: [PATCH 115/862] Allow ONVIF devices to resume a PullPoint subscription when the camera reboots (#37711) --- homeassistant/components/onvif/event.py | 91 ++++++++++++++++---- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 75 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 2516a4805e2..59076fd938d 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -1,8 +1,13 @@ """ONVIF event abstraction.""" +import asyncio import datetime as dt from typing import Callable, Dict, List, Optional, Set -from aiohttp.client_exceptions import ServerDisconnectedError +from aiohttp.client_exceptions import ( + ClientConnectorError, + ClientOSError, + ServerDisconnectedError, +) from onvif import ONVIFCamera, ONVIFService from zeep.exceptions import Fault @@ -15,6 +20,13 @@ from .models import Event from .parsers import PARSERS UNHANDLED_TOPICS = set() +SUBSCRIPTION_ERRORS = ( + ClientConnectorError, + ClientOSError, + Fault, + ServerDisconnectedError, + asyncio.TimeoutError, +) class EventManager: @@ -44,11 +56,7 @@ class EventManager: """Listen for data updates.""" # This is the first listener, set up polling. if not self._listeners: - self._unsub_refresh = async_track_point_in_utc_time( - self.hass, - self.async_pull_messages, - dt_util.utcnow() + dt.timedelta(seconds=1), - ) + self.schedule_pull() self._listeners.append(update_callback) @@ -89,12 +97,14 @@ class EventManager: ) self.started = True + return True - return self.started + return False async def async_stop(self) -> None: """Unsubscribe from events.""" self._listeners = [] + self.started = False if not self._subscription: return @@ -102,6 +112,40 @@ class EventManager: await self._subscription.Unsubscribe() self._subscription = None + async def async_restart(self, _now: dt = None) -> None: + """Restart the subscription assuming the camera rebooted.""" + if not self.started: + return + + if self._subscription: + try: + await self._subscription.Unsubscribe() + except SUBSCRIPTION_ERRORS: + pass # Ignored. The subscription may no longer exist. + self._subscription = None + + try: + restarted = await self.async_start() + except SUBSCRIPTION_ERRORS: + restarted = False + + if not restarted: + LOGGER.warning( + "Failed to restart ONVIF PullPoint subscription for '%s'. Retrying...", + self.unique_id, + ) + # Try again in a minute + self._unsub_refresh = async_track_point_in_utc_time( + self.hass, + self.async_restart, + dt_util.utcnow() + dt.timedelta(seconds=60), + ) + elif self._listeners: + LOGGER.info( + "Restarted ONVIF PullPoint subscription for '%s'", self.unique_id + ) + self.schedule_pull() + async def async_renew(self) -> None: """Renew subscription.""" if not self._subscription: @@ -114,6 +158,14 @@ class EventManager: ) await self._subscription.Renew(termination_time) + def schedule_pull(self) -> None: + """Schedule async_pull_messages to run.""" + self._unsub_refresh = async_track_point_in_utc_time( + self.hass, + self.async_pull_messages, + dt_util.utcnow() + dt.timedelta(seconds=1), + ) + async def async_pull_messages(self, _now: dt = None) -> None: """Pull messages from device.""" if self.hass.state == CoreState.running: @@ -129,14 +181,19 @@ class EventManager: dt_util.as_utc(response.TerminationTime) - dt_util.utcnow() ).total_seconds() < 7200: await self.async_renew() + except SUBSCRIPTION_ERRORS: + LOGGER.warning( + "Failed to fetch ONVIF PullPoint subscription messages for '%s'", + self.unique_id, + ) + # Treat errors as if the camera restarted. Assume that the pullpoint + # subscription is no longer valid. + self._unsub_refresh = None + await self.async_restart() + return - # Parse response - await self.async_parse_messages(response.NotificationMessage) - - except ServerDisconnectedError: - pass - except Fault: - pass + # Parse response + await self.async_parse_messages(response.NotificationMessage) # Update entities for update_callback in self._listeners: @@ -144,11 +201,7 @@ class EventManager: # Reschedule another pull if self._listeners: - self._unsub_refresh = async_track_point_in_utc_time( - self.hass, - self.async_pull_messages, - dt_util.utcnow() + dt.timedelta(seconds=1), - ) + self.schedule_pull() # pylint: disable=protected-access async def async_parse_messages(self, messages) -> None: diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index 4214cf3ab5c..182fe22d60c 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -2,7 +2,7 @@ "domain": "onvif", "name": "ONVIF", "documentation": "https://www.home-assistant.io/integrations/onvif", - "requirements": ["onvif-zeep-async==0.4.0", "WSDiscovery==2.0.0"], + "requirements": ["onvif-zeep-async==0.5.0", "WSDiscovery==2.0.0"], "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 79366b864f1..1a87b8973b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1005,7 +1005,7 @@ oemthermostat==1.1 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==0.4.0 +onvif-zeep-async==0.5.0 # homeassistant.components.opengarage open-garage==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ed7703d6f5..a3b91e324e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -473,7 +473,7 @@ numpy==1.19.1 oauth2client==4.0.0 # homeassistant.components.onvif -onvif-zeep-async==0.4.0 +onvif-zeep-async==0.5.0 # homeassistant.components.openerz openerz-api==0.1.0 From 6bdb2f3d11c70ffecaf94501950c075ddd6a986d Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 12 Aug 2020 00:23:51 +0200 Subject: [PATCH 116/862] Cleanup Netatmo code (#38772) * Clean up const * Clean up data handler --- homeassistant/components/netatmo/const.py | 5 ----- homeassistant/components/netatmo/data_handler.py | 1 - 2 files changed, 6 deletions(-) diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 30e40d358d9..f45fb09f94f 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -1,6 +1,4 @@ """Constants used by the Netatmo component.""" -from datetime import timedelta - API = "api" DOMAIN = "netatmo" @@ -70,9 +68,6 @@ ATTR_FACE_URL = "face_url" ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_NAME = "schedule_name" -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) - SERVICE_SET_SCHEDULE = "set_schedule" SERVICE_SET_PERSONS_HOME = "set_persons_home" SERVICE_SET_PERSON_AWAY = "set_person_away" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 8a299d0f072..5354e7cec75 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -36,7 +36,6 @@ DATA_CLASSES = { PUBLICDATA_DATA_CLASS_NAME: pyatmo.PublicData, } -MAX_CALLS_1H = 20 BATCH_SIZE = 3 DEFAULT_INTERVALS = { HOMEDATA_DATA_CLASS_NAME: 900, From b1fd931cdcff03656b2634f675968ac1034e66c5 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 11 Aug 2020 19:04:44 -0400 Subject: [PATCH 117/862] Add config flow to insteon component (#36467) * Squashed * Fix requirements_all * Update homeassistant/components/insteon/__init__.py Only update options if the result is to create the entry. Co-authored-by: J. Nick Koston * Update homeassistant/components/insteon/__init__.py No return value needed. Co-authored-by: J. Nick Koston * Ref RESULT_TYPE_CREATE_ENTRY correctly * Return result back to import config process * Make DOMAIN ref more clear Co-authored-by: J. Nick Koston --- .coveragerc | 13 +- homeassistant/components/insteon/__init__.py | 110 +-- .../components/insteon/binary_sensor.py | 25 +- homeassistant/components/insteon/climate.py | 25 +- .../components/insteon/config_flow.py | 317 ++++++++ homeassistant/components/insteon/const.py | 16 + homeassistant/components/insteon/cover.py | 21 +- homeassistant/components/insteon/fan.py | 21 +- .../components/insteon/insteon_entity.py | 30 + homeassistant/components/insteon/light.py | 21 +- .../components/insteon/manifest.json | 3 +- homeassistant/components/insteon/schemas.py | 185 ++++- homeassistant/components/insteon/strings.json | 115 +++ homeassistant/components/insteon/switch.py | 21 +- .../components/insteon/translations/en.json | 115 +++ homeassistant/components/insteon/utils.py | 105 ++- homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/insteon/__init__.py | 1 + tests/components/insteon/test_config_flow.py | 703 ++++++++++++++++++ 20 files changed, 1740 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/insteon/config_flow.py create mode 100644 homeassistant/components/insteon/strings.json create mode 100644 homeassistant/components/insteon/translations/en.json create mode 100644 tests/components/insteon/__init__.py create mode 100644 tests/components/insteon/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 3bf9397a80a..6809a58f157 100644 --- a/.coveragerc +++ b/.coveragerc @@ -394,7 +394,18 @@ omit = homeassistant/components/ihc/* homeassistant/components/imap/sensor.py homeassistant/components/imap_email_content/sensor.py - homeassistant/components/insteon/* + homeassistant/components/insteon/__init__.py + homeassistant/components/insteon/binary_sensor.py + homeassistant/components/insteon/climate.py + homeassistant/components/insteon/const.py + homeassistant/components/insteon/cover.py + homeassistant/components/insteon/fan.py + homeassistant/components/insteon/insteon_entity.py + homeassistant/components/insteon/ipdb.py + homeassistant/components/insteon/light.py + homeassistant/components/insteon/schemas.py + homeassistant/components/insteon/switch.py + homeassistant/components/insteon/utils.py homeassistant/components/incomfort/* homeassistant/components/intesishome/* homeassistant/components/ios/* diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index c28c04f589d..62032b681b8 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -4,24 +4,16 @@ import logging from pyinsteon import async_close, async_connect, devices -from homeassistant.const import ( - CONF_HOST, - CONF_PLATFORM, - CONF_PORT, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY +from homeassistant.exceptions import ConfigEntryNotReady from .const import ( CONF_CAT, CONF_DIM_STEPS, - CONF_FIRMWARE, CONF_HOUSECODE, - CONF_HUB_PASSWORD, - CONF_HUB_USERNAME, - CONF_HUB_VERSION, - CONF_IP_PORT, CONF_OVERRIDE, - CONF_PRODUCT_KEY, CONF_SUBCAT, CONF_UNITCODE, CONF_X10, @@ -29,7 +21,7 @@ from .const import ( INSTEON_COMPONENTS, ON_OFF_EVENTS, ) -from .schemas import CONFIG_SCHEMA # noqa F440 +from .schemas import convert_yaml_to_config_flow from .utils import ( add_on_off_event_device, async_register_services, @@ -63,10 +55,10 @@ async def async_id_unknown_devices(config_dir): await devices.async_save(workdir=config_dir) -async def async_setup_platforms(hass, config): +async def async_setup_platforms(hass, config_entry): """Initiate the connection and services.""" tasks = [ - hass.helpers.discovery.async_load_platform(component, DOMAIN, {}, config) + hass.config_entries.async_forward_entry_setup(config_entry, component) for component in INSTEON_COMPONENTS ] await asyncio.gather(*tasks) @@ -78,12 +70,26 @@ async def async_setup_platforms(hass, config): add_on_off_event_device(hass, device) _LOGGER.debug("Insteon device count: %s", len(devices)) - register_new_device_callback(hass, config) + register_new_device_callback(hass) async_register_services(hass) + device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, str(devices.modem.address))}, + manufacturer="Smart Home", + name=f"{devices.modem.description} {devices.modem.address}", + model=f"{devices.modem.model} (0x{devices.modem.cat:02x}, 0x{devices.modem.subcat:02x})", + sw_version=f"{devices.modem.firmware:02x} Engine Version: {devices.modem.engine_version}", + ) + + # Make a copy of addresses due to edge case where the list of devices could change during status update # Cannot be done concurrently due to issues with the underlying protocol. - for address in devices: - await devices[address].async_status() + for address in list(devices): + try: + await devices[address].async_status() + except AttributeError: + pass await async_id_unknown_devices(hass.config.config_dir) @@ -92,57 +98,57 @@ async def close_insteon_connection(*args): await async_close() +async def async_import_config(hass, conf): + """Set up all of the config imported from yaml.""" + data, options = convert_yaml_to_config_flow(conf) + # Create a config entry with the connection data + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=data + ) + # If this is the first time we ran, update the config options + if result["type"] == RESULT_TYPE_CREATE_ENTRY and options: + entry = result["result"] + hass.config_entries.async_update_entry( + entry=entry, options=options, + ) + return result + + async def async_setup(hass, config): - """Set up the connection to the modem.""" + """Set up the Insteon platform.""" + if DOMAIN not in config: + return True conf = config[DOMAIN] - port = conf.get(CONF_PORT) - host = conf.get(CONF_HOST) - ip_port = conf.get(CONF_IP_PORT) - username = conf.get(CONF_HUB_USERNAME) - password = conf.get(CONF_HUB_PASSWORD) - hub_version = conf.get(CONF_HUB_VERSION) + hass.async_create_task(async_import_config(hass, conf)) + return True - if host: - _LOGGER.info("Connecting to Insteon Hub on %s:%d", host, ip_port) - else: - _LOGGER.info("Connecting to Insteon PLM on %s", port) - try: - await async_connect( - device=port, - host=host, - port=ip_port, - username=username, - password=password, - hub_version=hub_version, - ) - except ConnectionError: - _LOGGER.error("Could not connect to Insteon modem") - return False - _LOGGER.info("Connection to Insteon modem successful") +async def async_setup_entry(hass, entry): + """Set up an Insteon entry.""" + + if not devices.modem: + try: + await async_connect(**entry.data) + except ConnectionError as exception: + _LOGGER.error("Could not connect to Insteon modem") + raise ConfigEntryNotReady from exception hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_insteon_connection) - conf = config[DOMAIN] - overrides = conf.get(CONF_OVERRIDE, []) - x10_devices = conf.get(CONF_X10, []) await devices.async_load( workdir=hass.config.config_dir, id_devices=0, load_modem_aldb=0 ) - for device_override in overrides: + for device_override in entry.options.get(CONF_OVERRIDE, []): # Override the device default capabilities for a specific address address = device_override.get("address") if not devices.get(address): cat = device_override[CONF_CAT] subcat = device_override[CONF_SUBCAT] - firmware = device_override.get(CONF_FIRMWARE) - if firmware is None: - firmware = device_override.get(CONF_PRODUCT_KEY, 0) - devices.set_id(address, cat, subcat, firmware) + devices.set_id(address, cat, subcat, 0) - for device in x10_devices: + for device in entry.options.get(CONF_X10, []): housecode = device.get(CONF_HOUSECODE) unitcode = device.get(CONF_UNITCODE) x10_type = "on_off" @@ -156,5 +162,5 @@ async def async_setup(hass, config): ) device = devices.add_x10_device(housecode, unitcode, x10_type, steps) - asyncio.create_task(async_setup_platforms(hass, config)) + asyncio.create_task(async_setup_platforms(hass, entry)) return True diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index cd74f738187..259fe20f7bc 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -26,10 +26,12 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, - DOMAIN, + DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorEntity, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import SIGNAL_ADD_ENTITIES from .insteon_entity import InsteonEntity from .utils import async_add_insteon_entities @@ -50,11 +52,22 @@ SENSOR_TYPES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON entity class for the hass platform.""" - async_add_insteon_entities( - hass, DOMAIN, InsteonBinarySensorEntity, async_add_entities, discovery_info - ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Insteon binary sensors from a config entry.""" + + def add_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, + BINARY_SENSOR_DOMAIN, + InsteonBinarySensorEntity, + async_add_entities, + discovery_info, + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{BINARY_SENSOR_DOMAIN}" + async_dispatcher_connect(hass, signal, add_entities) + add_entities() class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity): diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 79af0892f94..1b30554b82d 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -13,7 +13,7 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_FAN, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, - DOMAIN, + DOMAIN as CLIMATE_DOMAIN, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY, @@ -26,7 +26,9 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import SIGNAL_ADD_ENTITIES from .insteon_entity import InsteonEntity from .utils import async_add_insteon_entities @@ -62,11 +64,22 @@ SUPPORTED_FEATURES = ( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Insteon platform.""" - async_add_insteon_entities( - hass, DOMAIN, InsteonClimateEntity, async_add_entities, discovery_info - ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Insteon climate entities from a config entry.""" + + def add_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, + CLIMATE_DOMAIN, + InsteonClimateEntity, + async_add_entities, + discovery_info, + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{CLIMATE_DOMAIN}" + async_dispatcher_connect(hass, signal, add_entities) + add_entities() class InsteonClimateEntity(InsteonEntity, ClimateEntity): diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py new file mode 100644 index 00000000000..40e8b81f440 --- /dev/null +++ b/homeassistant/components/insteon/config_flow.py @@ -0,0 +1,317 @@ +"""Test config flow for Insteon.""" +import logging + +from pyinsteon import async_connect +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE, + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send + +# pylint: disable=unused-import +from .const import ( + CONF_HOUSECODE, + CONF_HUB_VERSION, + CONF_OVERRIDE, + CONF_UNITCODE, + CONF_X10, + DOMAIN, + SIGNAL_ADD_DEVICE_OVERRIDE, + SIGNAL_ADD_X10_DEVICE, + SIGNAL_REMOVE_DEVICE_OVERRIDE, + SIGNAL_REMOVE_X10_DEVICE, +) +from .schemas import ( + add_device_override, + add_x10_device, + build_device_override_schema, + build_hub_schema, + build_plm_schema, + build_remove_override_schema, + build_remove_x10_schema, + build_x10_schema, +) + +STEP_PLM = "plm" +STEP_HUB_V1 = "hubv1" +STEP_HUB_V2 = "hubv2" +STEP_CHANGE_HUB_CONFIG = "change_hub_config" +STEP_ADD_X10 = "add_x10" +STEP_ADD_OVERRIDE = "add_override" +STEP_REMOVE_OVERRIDE = "remove_override" +STEP_REMOVE_X10 = "remove_x10" +MODEM_TYPE = "modem_type" +PLM = "PowerLinc Modem (PLM)" +HUB1 = "Hub version 1 (pre-2014)" +HUB2 = "Hub version 2" + +_LOGGER = logging.getLogger(__name__) + + +def _only_one_selected(*args): + """Test if only one item is True.""" + return sum(args) == 1 + + +async def _async_connect(**kwargs): + """Connect to the Insteon modem.""" + try: + await async_connect(**kwargs) + _LOGGER.info("Connected to Insteon modem.") + return True + except ConnectionError: + _LOGGER.error("Could not connect to Insteon modem.") + return False + + +def _remove_override(address, options): + """Remove a device override from config.""" + new_options = {} + if options.get(CONF_X10): + new_options[CONF_X10] = options.get(CONF_X10) + new_overrides = [] + for override in options[CONF_OVERRIDE]: + if override[CONF_ADDRESS] != address: + new_overrides.append(override) + if new_overrides: + new_options[CONF_OVERRIDE] = new_overrides + return new_options + + +def _remove_x10(device, options): + """Remove an X10 device from the config.""" + housecode = device[11].lower() + unitcode = int(device[24:]) + new_options = {} + if options.get(CONF_OVERRIDE): + new_options[CONF_OVERRIDE] = options.get(CONF_OVERRIDE) + new_x10 = [] + for existing_device in options[CONF_X10]: + if ( + existing_device[CONF_HOUSECODE].lower() != housecode + or existing_device[CONF_UNITCODE] != unitcode + ): + new_x10.append(existing_device) + if new_x10: + new_options[CONF_X10] = new_x10 + return new_options, housecode, unitcode + + +class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Insteon config flow handler.""" + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Define the config flow to handle options.""" + return InsteonOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """For backward compatibility.""" + return await self.async_step_init(user_input=user_input) + + async def async_step_init(self, user_input=None): + """Init the config flow.""" + errors = {} + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + if user_input is not None: + selection = user_input.get(MODEM_TYPE) + + if selection == PLM: + return await self.async_step_plm() + if selection == HUB1: + return await self.async_step_hubv1() + return await self.async_step_hubv2() + modem_types = [PLM, HUB1, HUB2] + data_schema = vol.Schema({vol.Required(MODEM_TYPE): vol.In(modem_types)}) + return self.async_show_form( + step_id="init", data_schema=data_schema, errors=errors + ) + + async def async_step_plm(self, user_input=None): + """Set up the PLM modem type.""" + errors = {} + if user_input is not None: + if await _async_connect(**user_input): + return self.async_create_entry(title="", data=user_input) + errors["base"] = "cannot_connect" + schema_defaults = user_input if user_input is not None else {} + data_schema = build_plm_schema(**schema_defaults) + return self.async_show_form( + step_id=STEP_PLM, data_schema=data_schema, errors=errors + ) + + async def async_step_hubv1(self, user_input=None): + """Set up the Hub v1 modem type.""" + return await self._async_setup_hub(hub_version=1, user_input=user_input) + + async def async_step_hubv2(self, user_input=None): + """Set up the Hub v2 modem type.""" + return await self._async_setup_hub(hub_version=2, user_input=user_input) + + async def _async_setup_hub(self, hub_version, user_input): + """Set up the Hub versions 1 and 2.""" + errors = {} + if user_input is not None: + user_input[CONF_HUB_VERSION] = hub_version + if await _async_connect(**user_input): + return self.async_create_entry(title="", data=user_input) + user_input.pop(CONF_HUB_VERSION) + errors["base"] = "cannot_connect" + schema_defaults = user_input if user_input is not None else {} + data_schema = build_hub_schema(hub_version=hub_version, **schema_defaults) + step_id = STEP_HUB_V2 if hub_version == 2 else STEP_HUB_V1 + return self.async_show_form( + step_id=step_id, data_schema=data_schema, errors=errors + ) + + async def async_step_import(self, import_info): + """Import a yaml entry as a config entry.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + if not await _async_connect(**import_info): + return self.async_abort(reason="cannot_connect") + return self.async_create_entry(title="", data=import_info) + + +class InsteonOptionsFlowHandler(config_entries.OptionsFlow): + """Handle an Insteon options flow.""" + + def __init__(self, config_entry): + """Init the InsteonOptionsFlowHandler class.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Init the options config flow.""" + errors = {} + if user_input is not None: + change_hub_config = user_input.get(STEP_CHANGE_HUB_CONFIG, False) + device_override = user_input.get(STEP_ADD_OVERRIDE, False) + x10_device = user_input.get(STEP_ADD_X10, False) + remove_override = user_input.get(STEP_REMOVE_OVERRIDE, False) + remove_x10 = user_input.get(STEP_REMOVE_X10, False) + if _only_one_selected( + change_hub_config, + device_override, + x10_device, + remove_override, + remove_x10, + ): + if change_hub_config: + return await self.async_step_change_hub_config() + if device_override: + return await self.async_step_add_override() + if x10_device: + return await self.async_step_add_x10() + if remove_override: + return await self.async_step_remove_override() + if remove_x10: + return await self.async_step_remove_x10() + errors["base"] = "select_single" + + data_schema = { + vol.Optional(STEP_ADD_OVERRIDE): bool, + vol.Optional(STEP_ADD_X10): bool, + } + if self.config_entry.data.get(CONF_HOST): + data_schema[vol.Optional(STEP_CHANGE_HUB_CONFIG)] = bool + + options = {**self.config_entry.options} + if options.get(CONF_OVERRIDE): + data_schema[vol.Optional(STEP_REMOVE_OVERRIDE)] = bool + if options.get(CONF_X10): + data_schema[vol.Optional(STEP_REMOVE_X10)] = bool + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(data_schema), errors=errors + ) + + async def async_step_change_hub_config(self, user_input=None): + """Change the Hub configuration.""" + if user_input is not None: + data = { + **self.config_entry.data, + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + if self.config_entry.data[CONF_HUB_VERSION] == 2: + data[CONF_USERNAME] = user_input[CONF_USERNAME] + data[CONF_PASSWORD] = user_input[CONF_PASSWORD] + self.hass.config_entries.async_update_entry(self.config_entry, data=data) + return self.async_create_entry( + title="", data={**self.config_entry.options}, + ) + data_schema = build_hub_schema(**self.config_entry.data) + return self.async_show_form( + step_id=STEP_CHANGE_HUB_CONFIG, data_schema=data_schema + ) + + async def async_step_add_override(self, user_input=None): + """Add a device override.""" + errors = {} + if user_input is not None: + try: + data = add_device_override({**self.config_entry.options}, user_input) + async_dispatcher_send(self.hass, SIGNAL_ADD_DEVICE_OVERRIDE, user_input) + return self.async_create_entry(title="", data=data) + except ValueError: + errors["base"] = "input_error" + schema_defaults = user_input if user_input is not None else {} + data_schema = build_device_override_schema(**schema_defaults) + return self.async_show_form( + step_id=STEP_ADD_OVERRIDE, data_schema=data_schema, errors=errors + ) + + async def async_step_add_x10(self, user_input=None): + """Add an X10 device.""" + errors = {} + if user_input is not None: + options = add_x10_device({**self.config_entry.options}, user_input) + async_dispatcher_send(self.hass, SIGNAL_ADD_X10_DEVICE, user_input) + return self.async_create_entry(title="", data=options) + schema_defaults = user_input if user_input is not None else {} + data_schema = build_x10_schema(**schema_defaults) + return self.async_show_form( + step_id=STEP_ADD_X10, data_schema=data_schema, errors=errors + ) + + async def async_step_remove_override(self, user_input=None): + """Remove a device override.""" + errors = {} + options = self.config_entry.options + if user_input is not None: + options = _remove_override(user_input[CONF_ADDRESS], options) + async_dispatcher_send( + self.hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, user_input[CONF_ADDRESS], + ) + return self.async_create_entry(title="", data=options) + + data_schema = build_remove_override_schema(options[CONF_OVERRIDE]) + return self.async_show_form( + step_id=STEP_REMOVE_OVERRIDE, data_schema=data_schema, errors=errors + ) + + async def async_step_remove_x10(self, user_input=None): + """Remove an X10 device.""" + errors = {} + options = self.config_entry.options + if user_input is not None: + options, housecode, unitcode = _remove_x10(user_input[CONF_DEVICE], options) + async_dispatcher_send( + self.hass, SIGNAL_REMOVE_X10_DEVICE, housecode, unitcode + ) + return self.async_create_entry(title="", data=options) + + data_schema = build_remove_x10_schema(options[CONF_X10]) + return self.async_show_form( + step_id=STEP_REMOVE_X10, data_schema=data_schema, errors=errors + ) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index c55d733b73d..07717dade9b 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -43,6 +43,12 @@ INSTEON_COMPONENTS = [ "switch", ] +X10_PLATFORMS = [ + "binary_sensor", + "switch", + "light", +] + CONF_IP_PORT = "ip_port" CONF_HUB_USERNAME = "username" CONF_HUB_PASSWORD = "password" @@ -61,6 +67,9 @@ CONF_X10_ALL_UNITS_OFF = "x10_all_units_off" CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on" CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" +PORT_HUB_V1 = 9761 +PORT_HUB_V2 = 25105 + SRV_ADD_ALL_LINK = "add_all_link" SRV_DEL_ALL_LINK = "delete_all_link" SRV_LOAD_ALDB = "load_all_link_database" @@ -82,6 +91,13 @@ SRV_ADD_DEFAULT_LINKS = "add_default_links" SIGNAL_LOAD_ALDB = "load_aldb" SIGNAL_PRINT_ALDB = "print_aldb" SIGNAL_SAVE_DEVICES = "save_devices" +SIGNAL_ADD_ENTITIES = "insteon_add_entities" +SIGNAL_ADD_DEFAULT_LINKS = "add_default_links" +SIGNAL_ADD_DEVICE_OVERRIDE = "add_device_override" +SIGNAL_REMOVE_DEVICE_OVERRIDE = "insteon_remove_device_override" +SIGNAL_REMOVE_ENTITY = "insteon_remove_entity" +SIGNAL_ADD_X10_DEVICE = "insteon_add_x10_device" +SIGNAL_REMOVE_X10_DEVICE = "insteon_remove_x10_device" SIGNAL_ADD_DEFAULT_LINKS = "add_default_links" HOUSECODES = [ diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index 4f6fbd80dce..9d480937c9c 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -4,13 +4,15 @@ import math from homeassistant.components.cover import ( ATTR_POSITION, - DOMAIN, + DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, CoverEntity, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import SIGNAL_ADD_ENTITIES from .insteon_entity import InsteonEntity from .utils import async_add_insteon_entities @@ -19,11 +21,18 @@ _LOGGER = logging.getLogger(__name__) SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Insteon platform.""" - async_add_insteon_entities( - hass, DOMAIN, InsteonCoverEntity, async_add_entities, discovery_info - ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Insteon covers from a config entry.""" + + def add_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, COVER_DOMAIN, InsteonCoverEntity, async_add_entities, discovery_info + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{COVER_DOMAIN}" + async_dispatcher_connect(hass, signal, add_entities) + add_entities() class InsteonCoverEntity(InsteonEntity, CoverEntity): diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 3b324b97782..74271acfd4a 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -4,7 +4,7 @@ import logging from pyinsteon.constants import FanSpeed from homeassistant.components.fan import ( - DOMAIN, + DOMAIN as FAN_DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, @@ -12,7 +12,9 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import SIGNAL_ADD_ENTITIES from .insteon_entity import InsteonEntity from .utils import async_add_insteon_entities @@ -26,11 +28,18 @@ SPEED_TO_VALUE = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON entity class for the hass platform.""" - async_add_insteon_entities( - hass, DOMAIN, InsteonFanEntity, async_add_entities, discovery_info - ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Insteon fans from a config entry.""" + + def add_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, FAN_DOMAIN, InsteonFanEntity, async_add_entities, discovery_info + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{FAN_DOMAIN}" + async_dispatcher_connect(hass, signal, add_entities) + add_entities() class InsteonFanEntity(InsteonEntity, FanEntity): diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 1626482a80e..851675513da 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -1,6 +1,8 @@ """Insteon base entity.""" import logging +from pyinsteon import devices + from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -9,9 +11,11 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from .const import ( + DOMAIN, SIGNAL_ADD_DEFAULT_LINKS, SIGNAL_LOAD_ALDB, SIGNAL_PRINT_ALDB, + SIGNAL_REMOVE_ENTITY, SIGNAL_SAVE_DEVICES, STATE_NAME_LABEL_MAP, ) @@ -74,6 +78,18 @@ class InsteonEntity(Entity): """Provide attributes for display on device card.""" return {"insteon_address": self.address, "insteon_group": self.group} + @property + def device_info(self): + """Return device information.""" + return { + "identifiers": {(DOMAIN, str(self._insteon_device.address))}, + "name": f"{self._insteon_device.description} {self._insteon_device.address}", + "model": f"{self._insteon_device.model} (0x{self._insteon_device.cat:02x}, 0x{self._insteon_device.subcat:02x})", + "sw_version": f"{self._insteon_device.firmware:02x} Engine Version: {self._insteon_device.engine_version}", + "manufacturer": "Smart Home", + "via_device": (DOMAIN, str(devices.modem.address)), + } + @callback def async_entity_update(self, name, address, value, group): """Receive notification from transport that new data exists.""" @@ -101,6 +117,20 @@ class InsteonEntity(Entity): async_dispatcher_connect( self.hass, default_links_signal, self._async_add_default_links ) + remove_signal = f"{self._insteon_device.address.id}_{SIGNAL_REMOVE_ENTITY}" + self.async_on_remove( + async_dispatcher_connect(self.hass, remove_signal, self.async_remove) + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe to INSTEON update events.""" + _LOGGER.debug( + "Remove tracking updates for device %s group %d name %s", + self.address, + self.group, + self._insteon_device_group.name, + ) + self._insteon_device_group.unsubscribe(self.async_entity_update) async def _async_read_aldb(self, reload): """Call device load process and print to log.""" diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 5ad02b6da5e..a0ec04cda1b 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -3,11 +3,13 @@ import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, - DOMAIN, + DOMAIN as LIGHT_DOMAIN, SUPPORT_BRIGHTNESS, LightEntity, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import SIGNAL_ADD_ENTITIES from .insteon_entity import InsteonEntity from .utils import async_add_insteon_entities @@ -16,11 +18,18 @@ _LOGGER = logging.getLogger(__name__) MAX_BRIGHTNESS = 255 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Insteon component.""" - async_add_insteon_entities( - hass, DOMAIN, InsteonDimmerEntity, async_add_entities, discovery_info - ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Insteon lights from a config entry.""" + + def add_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, LIGHT_DOMAIN, InsteonDimmerEntity, async_add_entities, discovery_info + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{LIGHT_DOMAIN}" + async_dispatcher_connect(hass, signal, add_entities) + add_entities() class InsteonDimmerEntity(InsteonEntity, LightEntity): diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index cdcd07a403b..871629b6877 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -3,5 +3,6 @@ "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": ["pyinsteon==1.0.7"], - "codeowners": ["@teharris1"] + "codeowners": ["@teharris1"], + "config_flow": true } \ No newline at end of file diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index 0fe8f30d95b..5a293fb2e52 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -1,15 +1,20 @@ """Schemas used by insteon component.""" - +from binascii import Error as HexError, unhexlify from typing import Dict +from pyinsteon.address import Address +from pyinsteon.constants import HC_LOOKUP import voluptuous as vol from homeassistant.const import ( CONF_ADDRESS, + CONF_DEVICE, CONF_ENTITY_ID, CONF_HOST, + CONF_PASSWORD, CONF_PLATFORM, CONF_PORT, + CONF_USERNAME, ENTITY_MATCH_ALL, ) import homeassistant.helpers.config_validation as cv @@ -34,12 +39,15 @@ from .const import ( CONF_X10_ALL_UNITS_OFF, DOMAIN, HOUSECODES, + PORT_HUB_V1, + PORT_HUB_V2, SRV_ALL_LINK_GROUP, SRV_ALL_LINK_MODE, SRV_CONTROLLER, SRV_HOUSECODE, SRV_LOAD_DB_RELOAD, SRV_RESPONDER, + X10_PLATFORMS, ) @@ -51,7 +59,7 @@ def set_default_port(schema: Dict) -> Dict: if not ip_port: hub_version = schema.get(CONF_HUB_VERSION) # Found hub_version but not ip_port - schema[CONF_IP_PORT] = 9761 if hub_version == 1 else 25105 + schema[CONF_IP_PORT] = PORT_HUB_V1 if hub_version == 1 else PORT_HUB_V2 return schema @@ -150,3 +158,176 @@ TRIGGER_SCENE_SCHEMA = vol.Schema( ADD_DEFAULT_LINKS_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) + + +def normalize_byte_entry_to_int(entry: [int, bytes, str]): + """Format a hex entry value.""" + if isinstance(entry, int): + if entry in range(0, 256): + return entry + raise ValueError("Must be single byte") + if isinstance(entry, str): + if entry[0:2].lower() == "0x": + entry = entry[2:] + if len(entry) != 2: + raise ValueError("Not a valid hex code") + try: + entry = unhexlify(entry) + except HexError: + raise ValueError("Not a valid hex code") + return int.from_bytes(entry, byteorder="big") + + +def add_device_override(config_data, new_override): + """Add a new device override.""" + try: + address = str(Address(new_override[CONF_ADDRESS])) + cat = normalize_byte_entry_to_int(new_override[CONF_CAT]) + subcat = normalize_byte_entry_to_int(new_override[CONF_SUBCAT]) + except ValueError: + raise ValueError("Incorrect values") + + overrides = config_data.get(CONF_OVERRIDE, []) + curr_override = {} + + # If this address has an override defined, remove it + for override in overrides: + if override[CONF_ADDRESS] == address: + curr_override = override + break + if curr_override: + overrides.remove(curr_override) + + curr_override[CONF_ADDRESS] = address + curr_override[CONF_CAT] = cat + curr_override[CONF_SUBCAT] = subcat + overrides.append(curr_override) + config_data[CONF_OVERRIDE] = overrides + return config_data + + +def add_x10_device(config_data, new_x10): + """Add a new X10 device to X10 device list.""" + curr_device = {} + x10_devices = config_data.get(CONF_X10, []) + for x10_device in x10_devices: + if ( + x10_device[CONF_HOUSECODE] == new_x10[CONF_HOUSECODE] + and x10_device[CONF_UNITCODE] == new_x10[CONF_UNITCODE] + ): + curr_device = x10_device + break + + if curr_device: + x10_devices.remove(curr_device) + + curr_device[CONF_HOUSECODE] = new_x10[CONF_HOUSECODE] + curr_device[CONF_UNITCODE] = new_x10[CONF_UNITCODE] + curr_device[CONF_PLATFORM] = new_x10[CONF_PLATFORM] + curr_device[CONF_DIM_STEPS] = new_x10[CONF_DIM_STEPS] + x10_devices.append(curr_device) + config_data[CONF_X10] = x10_devices + return config_data + + +def build_device_override_schema( + address=vol.UNDEFINED, + cat=vol.UNDEFINED, + subcat=vol.UNDEFINED, + firmware=vol.UNDEFINED, +): + """Build the device override schema for config flow.""" + return vol.Schema( + { + vol.Required(CONF_ADDRESS, default=address): str, + vol.Optional(CONF_CAT, default=cat): str, + vol.Optional(CONF_SUBCAT, default=subcat): str, + } + ) + + +def build_x10_schema( + housecode=vol.UNDEFINED, + unitcode=vol.UNDEFINED, + platform=vol.UNDEFINED, + dim_steps=22, +): + """Build the X10 schema for config flow.""" + return vol.Schema( + { + vol.Required(CONF_HOUSECODE, default=housecode): vol.In(HC_LOOKUP.keys()), + vol.Required(CONF_UNITCODE, default=unitcode): vol.In(range(1, 17)), + vol.Required(CONF_PLATFORM, default=platform): vol.In(X10_PLATFORMS), + vol.Optional(CONF_DIM_STEPS, default=dim_steps): vol.In(range(1, 255)), + } + ) + + +def build_plm_schema(device=vol.UNDEFINED): + """Build the PLM schema for config flow.""" + return vol.Schema({vol.Required(CONF_DEVICE, default=device): str}) + + +def build_hub_schema( + hub_version, + host=vol.UNDEFINED, + port=PORT_HUB_V2, + username=vol.UNDEFINED, + password=vol.UNDEFINED, +): + """Build the Hub v2 schema for config flow.""" + schema = { + vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_PORT, default=port): int, + } + if hub_version == 2: + schema[vol.Required(CONF_USERNAME, default=username)] = str + schema[vol.Required(CONF_PASSWORD, default=password)] = str + return vol.Schema(schema) + + +def build_remove_override_schema(data): + """Build the schema to remove device overrides in config flow options.""" + selection = [] + for override in data: + selection.append(override[CONF_ADDRESS]) + return vol.Schema({vol.Required(CONF_ADDRESS): vol.In(selection)}) + + +def build_remove_x10_schema(data): + """Build the schema to remove an X10 device in config flow options.""" + selection = [] + for device in data: + housecode = device[CONF_HOUSECODE].upper() + unitcode = device[CONF_UNITCODE] + selection.append(f"Housecode: {housecode}, Unitcode: {unitcode}") + return vol.Schema({vol.Required(CONF_DEVICE): vol.In(selection)}) + + +def convert_yaml_to_config_flow(yaml_config): + """Convert the YAML based configuration to a config flow configuration.""" + config = {} + if yaml_config.get(CONF_HOST): + hub_version = yaml_config.get(CONF_HUB_VERSION, 2) + default_port = PORT_HUB_V2 if hub_version == 2 else PORT_HUB_V1 + config[CONF_HOST] = yaml_config.get(CONF_HOST) + config[CONF_PORT] = yaml_config.get(CONF_PORT, default_port) + config[CONF_HUB_VERSION] = hub_version + if hub_version == 2: + config[CONF_USERNAME] = yaml_config[CONF_USERNAME] + config[CONF_PASSWORD] = yaml_config[CONF_PASSWORD] + else: + config[CONF_DEVICE] = yaml_config[CONF_PORT] + + options = {} + for old_override in yaml_config.get(CONF_OVERRIDE, []): + override = {} + override[CONF_ADDRESS] = str(Address(old_override[CONF_ADDRESS])) + override[CONF_CAT] = normalize_byte_entry_to_int(old_override[CONF_CAT]) + override[CONF_SUBCAT] = normalize_byte_entry_to_int(old_override[CONF_SUBCAT]) + options = add_device_override(options, override) + + for x10_device in yaml_config.get(CONF_X10, []): + options = add_x10_device(options, x10_device) + + return config, options diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json new file mode 100644 index 00000000000..8df77e6c157 --- /dev/null +++ b/homeassistant/components/insteon/strings.json @@ -0,0 +1,115 @@ +{ + "config": { + "step": { + "init": { + "title": "Insteon", + "description": "Select the Insteon modem type.", + "data": { + "plm": "PowerLink Modem (PLM)", + "hubv1": "Hub Version 1 (Pre-2014)", + "hubv2": "Hub Version 2" + } + }, + "plm": { + "title": "Insteon PLM", + "description": "Configure the Insteon PowerLink Modem (PLM).", + "data": { + "device": "PLM device (i.e. /dev/ttyUSB0 or COM3)" + } + }, + "hub1": { + "title": "Insteon Hub Version 1", + "description": "Configure the Insteon Hub Version 1 (pre-2014).", + "data": { + "host": "Hub IP address", + "port": "IP port" + } + }, + "hub2": { + "title": "Insteon Hub Version 2", + "description": "Configure the Insteon Hub Version 2.", + "data": { + "host": "Hub IP address", + "username": "Username", + "password": "Password", + "port": "IP port" + } + } + }, + "error": { + "cannot_connect": "Failed to connect to the Insteon modem, please try again.", + "select_single": "Select one option." + }, + "abort": { + "cannot_connect": "Unable to connect to the Insteon modem", + "already_configured": "An Insteon modem connection is already configured" + } + }, + "options": { + "step": { + "init": { + "title": "Insteon", + "description": "Select an option to configure.", + "data": { + "change_hub_config": "Change the Hub configuration.", + "add_override": "Add a device override.", + "add_x10": "Add an X10 device.", + "remove_override": "Remove a device override.", + "remove_x10": "Remove an X10 device." + } + }, + "change_hub_config": { + "title": "Insteon", + "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app.", + "data": { + "host": "New host name or IP address", + "username": "New username", + "password": "New password", + "port": "New port number" + } + }, + "add_override": { + "title": "Insteon", + "description": "Add a device override.", + "data": { + "address": "Device address (i.e. 1a2b3c)", + "cat": "Device category (i.e. 0x10)", + "subcat": "Device subcategory (i.e. 0x0a)" + } + }, + "add_x10": { + "title": "Insteon", + "description": "Change the Insteon Hub password.", + "data": { + "housecode": "Housecode (a - p)", + "unitcode": "Unitcode (1 - 16)", + "platform": "Platform", + "steps": "Dimmer steps (for light devices only, default 22)" + } + }, + "remove_override": { + "title": "Insteon", + "description": "Remove a device override", + "data": { + "address": "Select a device address to remove" + } + }, + "remove_x10": { + "title": "Insteon", + "description": "Remove an X10 device", + "data": { + "address": "Select a device address to remove" + } + } + }, + "error": { + "cannot_connect": "Failed to connect to the Insteon modem, please try again.", + "select_single": "Select one option.", + "input_error": "Invalid entries, please check your values." + }, + "abort": { + "cannot_connect": "Unable to connect to the Insteon modem", + "already_configured": "An Insteon modem connection is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index 9d4e12b0b46..f0daf54990e 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -1,19 +1,28 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" import logging -from homeassistant.components.switch import DOMAIN, SwitchEntity +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import SIGNAL_ADD_ENTITIES from .insteon_entity import InsteonEntity from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON entity class for the hass platform.""" - async_add_insteon_entities( - hass, DOMAIN, InsteonSwitchEntity, async_add_entities, discovery_info - ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Insteon switches from a config entry.""" + + def add_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, SWITCH_DOMAIN, InsteonSwitchEntity, async_add_entities, discovery_info + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{SWITCH_DOMAIN}" + async_dispatcher_connect(hass, signal, add_entities) + add_entities() class InsteonSwitchEntity(InsteonEntity, SwitchEntity): diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json new file mode 100644 index 00000000000..92abb583a40 --- /dev/null +++ b/homeassistant/components/insteon/translations/en.json @@ -0,0 +1,115 @@ +{ + "config": { + "abort": { + "already_configured": "An Insteon modem connection is already configured", + "cannot_connect": "Unable to connect to the Insteon modem" + }, + "error": { + "cannot_connect": "Failed to connect to the Insteon modem, please try again.", + "select_single": "Select one option." + }, + "step": { + "hub1": { + "data": { + "host": "Hub IP address", + "port": "IP port" + }, + "description": "Configure the Insteon Hub Version 1 (pre-2014).", + "title": "Insteon Hub Version 1" + }, + "hub2": { + "data": { + "host": "Hub IP address", + "password": "Password", + "port": "IP port", + "username": "Username" + }, + "description": "Configure the Insteon Hub Version 2.", + "title": "Insteon Hub Version 2" + }, + "init": { + "data": { + "hubv1": "Hub Version 1 (Pre-2014)", + "hubv2": "Hub Version 2", + "plm": "PowerLink Modem (PLM)" + }, + "description": "Select the Insteon modem type.", + "title": "Insteon" + }, + "plm": { + "data": { + "device": "PLM device (i.e. /dev/ttyUSB0 or COM3)" + }, + "description": "Configure the Insteon PowerLink Modem (PLM).", + "title": "Insteon PLM" + } + } + }, + "options": { + "abort": { + "already_configured": "An Insteon modem connection is already configured", + "cannot_connect": "Unable to connect to the Insteon modem" + }, + "error": { + "cannot_connect": "Failed to connect to the Insteon modem, please try again.", + "input_error": "Invalid entries, please check your values.", + "select_single": "Select one option." + }, + "step": { + "add_override": { + "data": { + "address": "Device address (i.e. 1a2b3c)", + "cat": "Device category (i.e. 0x10)", + "subcat": "Device subcategory (i.e. 0x0a)" + }, + "description": "Add a device override.", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "Housecode (a - p)", + "platform": "Platform", + "steps": "Dimmer steps (for light devices only, default 22)", + "unitcode": "Unitcode (1 - 16)" + }, + "description": "Change the Insteon Hub password.", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "New host name or IP address", + "password": "New password", + "port": "New port number", + "username": "New username" + }, + "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Add a device override.", + "add_x10": "Add an X10 device.", + "change_hub_config": "Change the Hub configuration.", + "remove_override": "Remove a device override.", + "remove_x10": "Remove an X10 device." + }, + "description": "Select an option to configure.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Select a device address to remove" + }, + "description": "Remove a device override", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "Select a device address to remove" + }, + "description": "Remove an X10 device", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 19ea3dd46bd..7c7bf08792b 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -3,6 +3,7 @@ import asyncio import logging from pyinsteon import devices +from pyinsteon.address import Address from pyinsteon.constants import ALDBStatus from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT from pyinsteon.managers.link_manager import ( @@ -18,10 +19,15 @@ from pyinsteon.managers.x10_manager import ( async_x10_all_lights_on, async_x10_all_units_off, ) +from pyinsteon.x10_address import create as create_x10_address -from homeassistant.const import CONF_ADDRESS, CONF_ENTITY_ID, ENTITY_MATCH_ALL +from homeassistant.const import ( + CONF_ADDRESS, + CONF_ENTITY_ID, + CONF_PLATFORM, + ENTITY_MATCH_ALL, +) from homeassistant.core import callback -from homeassistant.helpers import discovery from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -29,6 +35,11 @@ from homeassistant.helpers.dispatcher import ( ) from .const import ( + CONF_CAT, + CONF_DIM_STEPS, + CONF_HOUSECODE, + CONF_SUBCAT, + CONF_UNITCODE, DOMAIN, EVENT_CONF_BUTTON, EVENT_GROUP_OFF, @@ -37,8 +48,14 @@ from .const import ( EVENT_GROUP_ON_FAST, ON_OFF_EVENTS, SIGNAL_ADD_DEFAULT_LINKS, + SIGNAL_ADD_DEVICE_OVERRIDE, + SIGNAL_ADD_ENTITIES, + SIGNAL_ADD_X10_DEVICE, SIGNAL_LOAD_ALDB, SIGNAL_PRINT_ALDB, + SIGNAL_REMOVE_DEVICE_OVERRIDE, + SIGNAL_REMOVE_ENTITY, + SIGNAL_REMOVE_X10_DEVICE, SIGNAL_SAVE_DEVICES, SRV_ADD_ALL_LINK, SRV_ADD_DEFAULT_LINKS, @@ -116,9 +133,8 @@ def add_on_off_event_device(hass, device): ) -def register_new_device_callback(hass, config): +def register_new_device_callback(hass): """Register callback for new Insteon device.""" - new_device_lock = asyncio.Lock() @callback def async_new_insteon_device(address=None): @@ -129,27 +145,17 @@ def register_new_device_callback(hass, config): _LOGGER.debug( "Adding new INSTEON device to Home Assistant with address %s", address ) - async with new_device_lock: - await devices.async_save(workdir=hass.config.config_dir) + await devices.async_save(workdir=hass.config.config_dir) device = devices[address] await device.async_status() platforms = get_device_platforms(device) - tasks = [] for platform in platforms: if platform == ON_OFF_EVENTS: add_on_off_event_device(hass, device) else: - tasks.append( - discovery.async_load_platform( - hass, - platform, - DOMAIN, - discovered={"address": device.address.id}, - hass_config=config, - ) - ) - await asyncio.gather(*tasks) + signal = f"{SIGNAL_ADD_ENTITIES}_{platform}" + dispatcher_send(hass, signal, {"address": device.address}) devices.subscribe(async_new_insteon_device, force_strong_ref=True) @@ -158,6 +164,8 @@ def register_new_device_callback(hass, config): def async_register_services(hass): """Register services used by insteon component.""" + save_lock = asyncio.Lock() + async def async_srv_add_all_link(service): """Add an INSTEON All-Link between two devices.""" group = service.data.get(SRV_ALL_LINK_GROUP) @@ -192,8 +200,9 @@ def async_register_services(hass): async def async_srv_save_devices(): """Write the Insteon device configuration to file.""" - _LOGGER.debug("Saving Insteon devices") - await devices.async_save(hass.config.config_dir) + async with save_lock: + _LOGGER.debug("Saving Insteon devices") + await devices.async_save(hass.config.config_dir) def print_aldb(service): """Print the All-Link Database for a device.""" @@ -241,6 +250,56 @@ def async_register_services(hass): signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}" async_dispatcher_send(hass, signal) + async def async_add_device_override(override): + """Remove an Insten device and associated entities.""" + address = Address(override[CONF_ADDRESS]) + await async_remove_device(address) + devices.set_id(address, override[CONF_CAT], override[CONF_SUBCAT], 0) + await async_srv_save_devices() + + async def async_remove_device_override(address): + """Remove an Insten device and associated entities.""" + address = Address(address) + await async_remove_device(address) + devices.set_id(address, None, None, None) + await devices.async_identify_device(address) + await async_srv_save_devices() + + @callback + def async_add_x10_device(x10_config): + """Add X10 device.""" + housecode = x10_config[CONF_HOUSECODE] + unitcode = x10_config[CONF_UNITCODE] + platform = x10_config[CONF_PLATFORM] + steps = x10_config.get(CONF_DIM_STEPS, 22) + x10_type = "on_off" + if platform == "light": + x10_type = "dimmable" + elif platform == "binary_sensor": + x10_type = "sensor" + _LOGGER.debug( + "Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type + ) + # This must be run in the event loop + devices.add_x10_device(housecode, unitcode, x10_type, steps) + + async def async_remove_x10_device(housecode, unitcode): + """Remove an X10 device and associated entities.""" + address = create_x10_address(housecode, unitcode) + devices.pop(address) + await async_remove_device(address) + + async def async_remove_device(address): + """Remove the device and all entities from hass.""" + signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}" + async_dispatcher_send(hass, signal) + dev_registry = await hass.helpers.device_registry.async_get_registry() + device = dev_registry.async_get_device( + identifiers={(DOMAIN, str(address))}, connections=set() + ) + if device: + dev_registry.async_remove_device(device.id) + hass.services.async_register( DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA ) @@ -286,6 +345,14 @@ def async_register_services(hass): schema=ADD_DEFAULT_LINKS_SCHEMA, ) async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices) + async_dispatcher_connect( + hass, SIGNAL_ADD_DEVICE_OVERRIDE, async_add_device_override + ) + async_dispatcher_connect( + hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, async_remove_device_override + ) + async_dispatcher_connect(hass, SIGNAL_ADD_X10_DEVICE, async_add_x10_device) + async_dispatcher_connect(hass, SIGNAL_REMOVE_X10_DEVICE, async_remove_x10_device) _LOGGER.debug("Insteon Services registered") diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3698fad422c..9b88a7bda0f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -85,6 +85,7 @@ FLOWS = [ "iaqualink", "icloud", "ifttt", + "insteon", "ios", "ipma", "ipp", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3b91e324e4..1f9d776342a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -664,6 +664,9 @@ pyhomematic==0.1.68 # homeassistant.components.icloud pyicloud==0.9.7 +# homeassistant.components.insteon +pyinsteon==1.0.7 + # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/insteon/__init__.py b/tests/components/insteon/__init__.py new file mode 100644 index 00000000000..91010bd1370 --- /dev/null +++ b/tests/components/insteon/__init__.py @@ -0,0 +1 @@ +"""Test for the Insteon integration.""" diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py new file mode 100644 index 00000000000..9b063d038eb --- /dev/null +++ b/tests/components/insteon/test_config_flow.py @@ -0,0 +1,703 @@ +"""Test the config flow for the Insteon integration.""" + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.insteon import async_import_config +from homeassistant.components.insteon.config_flow import ( + HUB1, + HUB2, + MODEM_TYPE, + PLM, + STEP_ADD_OVERRIDE, + STEP_ADD_X10, + STEP_CHANGE_HUB_CONFIG, + STEP_HUB_V2, + STEP_REMOVE_OVERRIDE, + STEP_REMOVE_X10, +) +from homeassistant.components.insteon.const import ( + CONF_CAT, + CONF_DIM_STEPS, + CONF_HOUSECODE, + CONF_HUB_VERSION, + CONF_OVERRIDE, + CONF_SUBCAT, + CONF_UNITCODE, + CONF_X10, + DOMAIN, + X10_PLATFORMS, +) +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE, + CONF_HOST, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +MOCK_HOSTNAME = "1.1.1.1" +MOCK_DEVICE = "/dev/ttyUSB55" +MOCK_USERNAME = "test-username" +MOCK_PASSWORD = "test-password" +MOCK_PORT = 4567 + +MOCK_ADDRESS = "1a2b3c" +MOCK_CAT = 0x02 +MOCK_SUBCAT = 0x1A + +MOCK_HOUSECODE = "c" +MOCK_UNITCODE = 6 +MOCK_X10_PLATFORM = X10_PLATFORMS[2] +MOCK_X10_STEPS = 10 + +MOCK_USER_INPUT_PLM = { + CONF_DEVICE: MOCK_DEVICE, +} + +MOCK_USER_INPUT_HUB_V2 = { + CONF_HOST: MOCK_HOSTNAME, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_PORT: MOCK_PORT, +} + +MOCK_USER_INPUT_HUB_V1 = { + CONF_HOST: MOCK_HOSTNAME, + CONF_PORT: MOCK_PORT, +} + +MOCK_DEVICE_OVERRIDE_CONFIG = { + CONF_ADDRESS: MOCK_ADDRESS, + CONF_CAT: MOCK_CAT, + CONF_SUBCAT: MOCK_SUBCAT, +} + +MOCK_X10_CONFIG = { + CONF_HOUSECODE: MOCK_HOUSECODE, + CONF_UNITCODE: MOCK_UNITCODE, + CONF_PLATFORM: MOCK_X10_PLATFORM, + CONF_DIM_STEPS: MOCK_X10_STEPS, +} + +MOCK_IMPORT_CONFIG_PLM = {CONF_PORT: MOCK_DEVICE} + +MOCK_IMPORT_MINIMUM_HUB_V2 = { + CONF_HOST: MOCK_HOSTNAME, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, +} +MOCK_IMPORT_MINIMUM_HUB_V1 = {CONF_HOST: MOCK_HOSTNAME, CONF_HUB_VERSION: 1} +MOCK_IMPORT_FULL_CONFIG_PLM = MOCK_IMPORT_CONFIG_PLM.copy() +MOCK_IMPORT_FULL_CONFIG_PLM[CONF_OVERRIDE] = [MOCK_DEVICE_OVERRIDE_CONFIG] +MOCK_IMPORT_FULL_CONFIG_PLM[CONF_X10] = [MOCK_X10_CONFIG] + +MOCK_IMPORT_FULL_CONFIG_HUB_V2 = MOCK_USER_INPUT_HUB_V2.copy() +MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_HUB_VERSION] = 2 +MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_OVERRIDE] = [MOCK_DEVICE_OVERRIDE_CONFIG] +MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_X10] = [MOCK_X10_CONFIG] + +MOCK_IMPORT_FULL_CONFIG_HUB_V1 = MOCK_USER_INPUT_HUB_V1.copy() +MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_HUB_VERSION] = 1 +MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_OVERRIDE] = [MOCK_DEVICE_OVERRIDE_CONFIG] +MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_X10] = [MOCK_X10_CONFIG] + +PATCH_CONNECTION = "homeassistant.components.insteon.config_flow.async_connect" +PATCH_ASYNC_SETUP = "homeassistant.components.insteon.async_setup" +PATCH_ASYNC_SETUP_ENTRY = "homeassistant.components.insteon.async_setup_entry" + + +async def mock_successful_connection(*args, **kwargs): + """Return a successful connection.""" + return True + + +async def mock_failed_connection(*args, **kwargs): + """Return a failed connection.""" + raise ConnectionError("Connection failed") + + +async def _init_form(hass, modem_type): + """Run the init form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {MODEM_TYPE: modem_type}, + ) + return result2 + + +async def _device_form(hass, flow_id, connection, user_input): + """Test the PLM, Hub v1 or Hub v2 form.""" + with patch(PATCH_CONNECTION, new=connection,), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure(flow_id, user_input) + return result, mock_setup, mock_setup_entry + + +async def test_form_select_modem(hass: HomeAssistantType): + """Test we get a modem form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, HUB2) + assert result["step_id"] == STEP_HUB_V2 + assert result["type"] == "form" + + +async def test_fail_on_existing(hass: HomeAssistantType): + """Test we fail if the integration is already configured.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={}, + ) + config_entry.add_to_hass(hass) + assert config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_form_select_plm(hass: HomeAssistantType): + """Test we set up the PLM correctly.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, PLM) + + result2, mock_setup, mock_setup_entry = await _device_form( + hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_PLM + ) + assert result2["type"] == "create_entry" + assert result2["data"] == MOCK_USER_INPUT_PLM + + 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_select_hub_v1(hass: HomeAssistantType): + """Test we set up the Hub v1 correctly.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, HUB1) + + result2, mock_setup, mock_setup_entry = await _device_form( + hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_HUB_V1 + ) + assert result2["type"] == "create_entry" + assert result2["data"] == { + **MOCK_USER_INPUT_HUB_V1, + CONF_HUB_VERSION: 1, + } + + 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_select_hub_v2(hass: HomeAssistantType): + """Test we set up the Hub v2 correctly.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, HUB2) + + result2, mock_setup, mock_setup_entry = await _device_form( + hass, result["flow_id"], mock_successful_connection, MOCK_USER_INPUT_HUB_V2 + ) + assert result2["type"] == "create_entry" + assert result2["data"] == { + **MOCK_USER_INPUT_HUB_V2, + CONF_HUB_VERSION: 2, + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_failed_connection_plm(hass: HomeAssistantType): + """Test a failed connection with the PLM.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, PLM) + + result2, _, _ = await _device_form( + hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_PLM + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_failed_connection_hub(hass: HomeAssistantType): + """Test a failed connection with a Hub.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, HUB2) + + result2, _, _ = await _device_form( + hass, result["flow_id"], mock_failed_connection, MOCK_USER_INPUT_HUB_V2 + ) + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def _import_config(hass, config): + """Run the import step.""" + with patch(PATCH_CONNECTION, new=mock_successful_connection,), patch( + PATCH_ASYNC_SETUP, return_value=True + ), patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): + return await async_import_config(hass, config) + + +async def test_import_plm(hass: HomeAssistantType): + """Test importing a minimum PLM config from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await _import_config(hass, MOCK_IMPORT_CONFIG_PLM) + + assert result["type"] == "create_entry" + assert hass.config_entries.async_entries(DOMAIN) + for entry in hass.config_entries.async_entries(DOMAIN): + assert entry.data == MOCK_USER_INPUT_PLM + + +async def test_import_plm_full(hass: HomeAssistantType): + """Test importing a full PLM config from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _import_config(hass, MOCK_IMPORT_FULL_CONFIG_PLM) + assert result["type"] == "create_entry" + assert hass.config_entries.async_entries(DOMAIN) + for entry in hass.config_entries.async_entries(DOMAIN): + assert entry.data == MOCK_USER_INPUT_PLM + assert entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" + assert entry.options[CONF_OVERRIDE][0][CONF_CAT] == MOCK_CAT + assert entry.options[CONF_OVERRIDE][0][CONF_SUBCAT] == MOCK_SUBCAT + assert entry.options[CONF_X10][0][CONF_HOUSECODE] == MOCK_HOUSECODE + assert entry.options[CONF_X10][0][CONF_UNITCODE] == MOCK_UNITCODE + assert entry.options[CONF_X10][0][CONF_PLATFORM] == MOCK_X10_PLATFORM + assert entry.options[CONF_X10][0][CONF_DIM_STEPS] == MOCK_X10_STEPS + + +async def test_import_full_hub_v1(hass: HomeAssistantType): + """Test importing a full Hub v1 config from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await _import_config(hass, MOCK_IMPORT_FULL_CONFIG_HUB_V1) + + assert result["type"] == "create_entry" + assert hass.config_entries.async_entries(DOMAIN) + for entry in hass.config_entries.async_entries(DOMAIN): + assert entry.data[CONF_HOST] == MOCK_HOSTNAME + assert entry.data[CONF_PORT] == MOCK_PORT + assert entry.data[CONF_HUB_VERSION] == 1 + assert CONF_USERNAME not in entry.data + assert CONF_PASSWORD not in entry.data + assert CONF_OVERRIDE not in entry.data + assert CONF_X10 not in entry.data + assert entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" + assert entry.options[CONF_X10][0][CONF_HOUSECODE] == MOCK_HOUSECODE + + +async def test_import_full_hub_v2(hass: HomeAssistantType): + """Test importing a full Hub v2 config from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await _import_config(hass, MOCK_IMPORT_FULL_CONFIG_HUB_V2) + + assert result["type"] == "create_entry" + assert hass.config_entries.async_entries(DOMAIN) + for entry in hass.config_entries.async_entries(DOMAIN): + assert entry.data[CONF_HOST] == MOCK_HOSTNAME + assert entry.data[CONF_PORT] == MOCK_PORT + assert entry.data[CONF_USERNAME] == MOCK_USERNAME + assert entry.data[CONF_PASSWORD] == MOCK_PASSWORD + assert entry.data[CONF_HUB_VERSION] == 2 + assert CONF_OVERRIDE not in entry.data + assert CONF_X10 not in entry.data + assert entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" + assert entry.options[CONF_X10][0][CONF_HOUSECODE] == MOCK_HOUSECODE + + +async def test_import_min_hub_v2(hass: HomeAssistantType): + """Test importing a minimum Hub v2 config from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await _import_config(hass, MOCK_IMPORT_MINIMUM_HUB_V2) + + assert result["type"] == "create_entry" + assert hass.config_entries.async_entries(DOMAIN) + for entry in hass.config_entries.async_entries(DOMAIN): + assert entry.data[CONF_HOST] == MOCK_HOSTNAME + assert entry.data[CONF_PORT] == 25105 + assert entry.data[CONF_USERNAME] == MOCK_USERNAME + assert entry.data[CONF_PASSWORD] == MOCK_PASSWORD + assert entry.data[CONF_HUB_VERSION] == 2 + + +async def test_import_min_hub_v1(hass: HomeAssistantType): + """Test importing a minimum Hub v1 config from yaml.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await _import_config(hass, MOCK_IMPORT_MINIMUM_HUB_V1) + + assert result["type"] == "create_entry" + assert hass.config_entries.async_entries(DOMAIN) + for entry in hass.config_entries.async_entries(DOMAIN): + assert entry.data[CONF_HOST] == MOCK_HOSTNAME + assert entry.data[CONF_PORT] == 9761 + assert entry.data[CONF_HUB_VERSION] == 1 + + +async def test_import_existing(hass: HomeAssistantType): + """Test we fail on an existing config imported.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={}, + ) + config_entry.add_to_hass(hass) + assert config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED + + result = await _import_config(hass, MOCK_IMPORT_MINIMUM_HUB_V2) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_import_failed_connection(hass: HomeAssistantType): + """Test a failed connection on import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch(PATCH_CONNECTION, new=mock_failed_connection,), patch( + PATCH_ASYNC_SETUP, return_value=True + ), patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): + result = await async_import_config(hass, MOCK_IMPORT_MINIMUM_HUB_V2) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def _options_init_form(hass, entry_id, step): + """Run the init options form.""" + with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): + result = await hass.config_entries.options.async_init(entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], {step: True}, + ) + return result2 + + +async def _options_form(hass, flow_id, user_input): + """Test an options form.""" + + with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True) as mock_setup_entry: + result = await hass.config_entries.options.async_configure(flow_id, user_input) + return result, mock_setup_entry + + +async def test_options_change_hub_config(hass: HomeAssistantType): + """Test changing Hub v2 config.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={}, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form( + hass, config_entry.entry_id, STEP_CHANGE_HUB_CONFIG + ) + + user_input = { + CONF_HOST: "2.3.4.5", + CONF_PORT: 9999, + CONF_USERNAME: "new username", + CONF_PASSWORD: "new password", + } + result, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {} + assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} + + +async def test_options_add_device_override(hass: HomeAssistantType): + """Test adding a device override.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={}, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_OVERRIDE) + + user_input = { + CONF_ADDRESS: "1a2b3c", + CONF_CAT: "0x04", + CONF_SUBCAT: "0xaa", + } + result, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(config_entry.options[CONF_OVERRIDE]) == 1 + assert config_entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" + assert config_entry.options[CONF_OVERRIDE][0][CONF_CAT] == 4 + assert config_entry.options[CONF_OVERRIDE][0][CONF_SUBCAT] == 170 + + result2 = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_OVERRIDE) + + user_input = { + CONF_ADDRESS: "4d5e6f", + CONF_CAT: "05", + CONF_SUBCAT: "bb", + } + await _options_form(hass, result2["flow_id"], user_input) + + assert len(config_entry.options[CONF_OVERRIDE]) == 2 + assert config_entry.options[CONF_OVERRIDE][1][CONF_ADDRESS] == "4D.5E.6F" + assert config_entry.options[CONF_OVERRIDE][1][CONF_CAT] == 5 + assert config_entry.options[CONF_OVERRIDE][1][CONF_SUBCAT] == 187 + + +async def test_options_remove_device_override(hass: HomeAssistantType): + """Test removing a device override.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={ + CONF_OVERRIDE: [ + {CONF_ADDRESS: "1A.2B.3C", CONF_CAT: 6, CONF_SUBCAT: 100}, + {CONF_ADDRESS: "4D.5E.6F", CONF_CAT: 7, CONF_SUBCAT: 200}, + ] + }, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_OVERRIDE) + + user_input = {CONF_ADDRESS: "1A.2B.3C"} + result, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(config_entry.options[CONF_OVERRIDE]) == 1 + + +async def test_options_remove_device_override_with_x10(hass: HomeAssistantType): + """Test removing a device override when an X10 device is configured.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={ + CONF_OVERRIDE: [ + {CONF_ADDRESS: "1A.2B.3C", CONF_CAT: 6, CONF_SUBCAT: 100}, + {CONF_ADDRESS: "4D.5E.6F", CONF_CAT: 7, CONF_SUBCAT: 200}, + ], + CONF_X10: [ + { + CONF_HOUSECODE: "d", + CONF_UNITCODE: 5, + CONF_PLATFORM: "light", + CONF_DIM_STEPS: 22, + } + ], + }, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_OVERRIDE) + + user_input = {CONF_ADDRESS: "1A.2B.3C"} + result, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(config_entry.options[CONF_OVERRIDE]) == 1 + assert len(config_entry.options[CONF_X10]) == 1 + + +async def test_options_add_x10_device(hass: HomeAssistantType): + """Test adding an X10 device.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={}, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_X10) + + user_input = { + CONF_HOUSECODE: "c", + CONF_UNITCODE: 12, + CONF_PLATFORM: "light", + CONF_DIM_STEPS: 18, + } + result2, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(config_entry.options[CONF_X10]) == 1 + assert config_entry.options[CONF_X10][0][CONF_HOUSECODE] == "c" + assert config_entry.options[CONF_X10][0][CONF_UNITCODE] == 12 + assert config_entry.options[CONF_X10][0][CONF_PLATFORM] == "light" + assert config_entry.options[CONF_X10][0][CONF_DIM_STEPS] == 18 + + result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_X10) + user_input = { + CONF_HOUSECODE: "d", + CONF_UNITCODE: 10, + CONF_PLATFORM: "binary_sensor", + CONF_DIM_STEPS: 15, + } + result3, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(config_entry.options[CONF_X10]) == 2 + assert config_entry.options[CONF_X10][1][CONF_HOUSECODE] == "d" + assert config_entry.options[CONF_X10][1][CONF_UNITCODE] == 10 + assert config_entry.options[CONF_X10][1][CONF_PLATFORM] == "binary_sensor" + assert config_entry.options[CONF_X10][1][CONF_DIM_STEPS] == 15 + + +async def test_options_remove_x10_device(hass: HomeAssistantType): + """Test removing an X10 device.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={ + CONF_X10: [ + { + CONF_HOUSECODE: "C", + CONF_UNITCODE: 4, + CONF_PLATFORM: "light", + CONF_DIM_STEPS: 18, + }, + { + CONF_HOUSECODE: "D", + CONF_UNITCODE: 10, + CONF_PLATFORM: "binary_sensor", + CONF_DIM_STEPS: 15, + }, + ] + }, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_X10) + + for device in config_entry.options[CONF_X10]: + housecode = device[CONF_HOUSECODE].upper() + unitcode = device[CONF_UNITCODE] + print(f"Housecode: {housecode}, Unitcode: {unitcode}") + + user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} + result, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(config_entry.options[CONF_X10]) == 1 + + +async def test_options_remove_x10_device_with_override(hass: HomeAssistantType): + """Test removing an X10 device when a device override is configured.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={ + CONF_X10: [ + { + CONF_HOUSECODE: "C", + CONF_UNITCODE: 4, + CONF_PLATFORM: "light", + CONF_DIM_STEPS: 18, + }, + { + CONF_HOUSECODE: "D", + CONF_UNITCODE: 10, + CONF_PLATFORM: "binary_sensor", + CONF_DIM_STEPS: 15, + }, + ], + CONF_OVERRIDE: [{CONF_ADDRESS: "1A.2B.3C", CONF_CAT: 1, CONF_SUBCAT: 18}], + }, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form(hass, config_entry.entry_id, STEP_REMOVE_X10) + + for device in config_entry.options[CONF_X10]: + housecode = device[CONF_HOUSECODE].upper() + unitcode = device[CONF_UNITCODE] + print(f"Housecode: {housecode}, Unitcode: {unitcode}") + + user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} + result, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(config_entry.options[CONF_X10]) == 1 + assert len(config_entry.options[CONF_OVERRIDE]) == 1 + + +async def test_options_dup_selection(hass: HomeAssistantType): + """Test if a duplicate selection was made in options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={}, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], {STEP_ADD_OVERRIDE: True, STEP_ADD_X10: True}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "select_single"} + + +async def test_options_override_bad_data(hass: HomeAssistantType): + """Test for bad data in a device override.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, + options={}, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form(hass, config_entry.entry_id, STEP_ADD_OVERRIDE) + + user_input = { + CONF_ADDRESS: "zzzzzz", + CONF_CAT: "bad", + CONF_SUBCAT: "data", + } + result, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "input_error"} From b242b468880dc2e652d537ba79c7e4fd673c28bd Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 11 Aug 2020 20:41:49 -0400 Subject: [PATCH 118/862] Bump up ZHA dependencies (#38775) --- 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 3b123c53598..b9d2caf0137 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.18.0", + "bellows==0.18.1", "pyserial==3.4", "zha-quirks==0.0.43", "zigpy-cc==0.4.4", diff --git a/requirements_all.txt b/requirements_all.txt index 1a87b8973b4..7941ef2fa5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -334,7 +334,7 @@ beautifulsoup4==4.9.0 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows==0.18.0 +bellows==0.18.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f9d776342a..e5fe88d13f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,7 +181,7 @@ azure-eventhub==5.1.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.18.0 +bellows==0.18.1 # homeassistant.components.blebox blebox_uniapi==1.3.2 From 540d0e5428cee27a9a74bf7888f2bfd234d80661 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 11 Aug 2020 20:27:16 -0700 Subject: [PATCH 119/862] Bump androidtv to 0.0.49 (#38778) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 5b612c3f4c7..8e62813714e 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.2.1", - "androidtv[async]==0.0.48", + "androidtv[async]==0.0.49", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 7941ef2fa5b..9f538fb497b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.48 +androidtv[async]==0.0.49 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5fe88d13f9..1e766138092 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -147,7 +147,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.48 +androidtv[async]==0.0.49 # homeassistant.components.apns apns2==0.3.0 From 5f95b5caaf80ee463a9e79f0afb47969c95bd261 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 12 Aug 2020 08:00:38 +0200 Subject: [PATCH 120/862] Fix lastest version in updater for Supervisor enabled installs (#38773) * Fix lastest version in update for Supervisor enabled installs * Fix updater tests --- homeassistant/components/hassio/__init__.py | 24 ++++++++++---------- homeassistant/components/hassio/handler.py | 8 +++++++ homeassistant/components/updater/__init__.py | 5 ++-- tests/components/hassio/test_handler.py | 24 ++++++++++++++++++++ tests/components/hassio/test_init.py | 20 +++++++++------- tests/components/updater/test_init.py | 7 +----- 6 files changed, 60 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index f64461f70d3..69c53225d49 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -44,6 +44,7 @@ CONFIG_SCHEMA = vol.Schema( DATA_INFO = "hassio_info" DATA_HOST_INFO = "hassio_host_info" +DATA_CORE_INFO = "hassio_core_info" HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) SERVICE_ADDON_START = "addon_start" @@ -140,18 +141,6 @@ async def async_get_addon_info(hass: HomeAssistantType, addon_id: str) -> dict: return result["data"] -@callback -@bind_hass -def get_homeassistant_version(hass): - """Return latest available Home Assistant version. - - Async friendly. - """ - if DATA_INFO not in hass.data: - return None - return hass.data[DATA_INFO].get("homeassistant") - - @callback @bind_hass def get_info(hass): @@ -172,6 +161,16 @@ def get_host_info(hass): return hass.data.get(DATA_HOST_INFO) +@callback +@bind_hass +def get_core_info(hass): + """Return Home Assistant Core information from Supervisor. + + Async friendly. + """ + return hass.data.get(DATA_CORE_INFO) + + @callback @bind_hass def is_hassio(hass): @@ -301,6 +300,7 @@ async def async_setup(hass, config): try: hass.data[DATA_INFO] = await hassio.get_info() hass.data[DATA_HOST_INFO] = await hassio.get_host_info() + hass.data[DATA_CORE_INFO] = await hassio.get_core_info() except HassioAPIError as err: _LOGGER.warning("Can't read last version: %s", err) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 861056a46e4..e96ed613324 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -82,6 +82,14 @@ class HassIO: """ return self.send_command("/host/info", method="get") + @_api_data + def get_core_info(self): + """Return data for Home Asssistant Core. + + This method returns a coroutine. + """ + return self.send_command("/core/info", method="get") + @_api_data def get_addon_info(self, addon): """Return data for a Add-on. diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index d90efe132f6..f3c9483e4a8 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -76,9 +76,10 @@ async def async_setup(hass, config): if "dev" in current_version: return Updater(False, "", "") - # Load data from supervisor on Hass.io + # Load data from Supervisor if hass.components.hassio.is_hassio(): - newest = hass.components.hassio.get_homeassistant_version() + core_info = hass.components.hassio.get_core_info() + newest = core_info["version_latest"] # Validate version update_available = False diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 67fcfb75d5f..311fc6c7e8c 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -92,6 +92,30 @@ async def test_api_host_info_error(hassio_handler, aioclient_mock): assert aioclient_mock.call_count == 1 +async def test_api_core_info(hassio_handler, aioclient_mock): + """Test setup with API Home Assistant Core info.""" + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) + + data = await hassio_handler.get_core_info() + assert aioclient_mock.call_count == 1 + assert data["version_latest"] == "1.0.0" + + +async def test_api_core_info_error(hassio_handler, aioclient_mock): + """Test setup with API Home Assistant Core info error.""" + aioclient_mock.get( + "http://127.0.0.1/core/info", json={"result": "error", "message": None} + ) + + with pytest.raises(HassioAPIError): + await hassio_handler.get_core_info() + + assert aioclient_mock.call_count == 1 + + async def test_api_homeassistant_stop(hassio_handler, aioclient_mock): """Test setup with API Home Assistant stop.""" aioclient_mock.post("http://127.0.0.1/homeassistant/stop", json={"result": "ok"}) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index d0043747835..34ba638410a 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -40,6 +40,10 @@ def mock_all(aioclient_mock): }, }, ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) @@ -51,8 +55,8 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 6 - assert hass.components.hassio.get_homeassistant_version() == "0.110.0" + assert aioclient_mock.call_count == 7 + assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -90,7 +94,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -106,7 +110,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -118,7 +122,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -165,7 +169,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -179,7 +183,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" await hass.config.async_update(time_zone="America/New_York") @@ -195,7 +199,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index 203a4df8355..89ebf9e1bbb 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -154,12 +154,7 @@ async def test_new_version_shows_entity_after_hour_hassio( """Test if binary sensor gets updated if new version is available / Hass.io.""" mock_get_uuid.return_value = MOCK_HUUID mock_component(hass, "hassio") - hass.data["hassio_info"] = {"hassos": None, "homeassistant": "999.0"} - hass.data["hassio_host"] = { - "supervisor": "222", - "chassis": "vm", - "operating_system": "HassOS 4.6", - } + hass.data["hassio_core_info"] = {"version_latest": "999.0"} assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) From 4833cf4e4a82438ee1d2d1eb264532ec873f5e0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2020 08:29:52 +0200 Subject: [PATCH 121/862] Bump actions/setup-python from v2.1.1 to v2.1.2 (#38780) Bumps [actions/setup-python](https://github.com/actions/setup-python) from v2.1.1 to v2.1.2. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2.1.1...24156c231c5e9d581bde27d0cdbb72715060ea51) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6389a542825..4f23b27083a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -75,7 +75,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -119,7 +119,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -163,7 +163,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -229,7 +229,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -276,7 +276,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -323,7 +323,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -367,7 +367,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -414,7 +414,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -469,7 +469,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -516,7 +516,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -548,7 +548,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.1 + uses: actions/setup-python@v2.1.2 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} From 7ac38557e638476cbfb6e7aa167f8b88cae3056f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2020 08:30:13 +0200 Subject: [PATCH 122/862] Bump actions/upload-artifact from v2.1.3 to v2.1.4 (#38779) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from v2.1.3 to v2.1.4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2.1.3...58740802ef971a2d71eff71e63d48ab68d1f5507) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4f23b27083a..2fd37625938 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -737,7 +737,7 @@ jobs: -p no:sugar \ tests - name: Upload coverage artifact - uses: actions/upload-artifact@v2.1.3 + uses: actions/upload-artifact@v2.1.4 with: name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} path: .coverage From f286992b1007588ae1221352883259393947fdfe Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 12 Aug 2020 08:41:11 +0200 Subject: [PATCH 123/862] Remove Netatmo HomeKit discovery method (#38770) --- homeassistant/components/netatmo/config_flow.py | 4 ---- tests/components/netatmo/test_config_flow.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 516f78e8019..c78a5169d3b 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -74,10 +74,6 @@ class NetatmoFlowHandler( return await super().async_step_user(user_input) - async def async_step_homekit(self, homekit_info): - """Handle HomeKit discovery.""" - return await self.async_step_user() - class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): """Handle Netatmo options.""" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index c6091e4d5e1..53922ead93c 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -39,7 +39,7 @@ async def test_abort_if_existing_entry(hass): data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" + assert result["reason"] == "already_configured" async def test_full_flow(hass, aiohttp_client, aioclient_mock): From e9b50706a939c45ff1ac675e6f4de65d7cbb26fd Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Wed, 12 Aug 2020 14:09:47 +0100 Subject: [PATCH 124/862] Add roon media player integration (#37553) * Import roon code. * Fix flake8/pylint issues. * Fix lint issues, extend timeout, change contact infomation. * Add new files to .coveragerc * Make file executable. * Fix problem with integration not working after initial creation. * Improve logic unavailable players by caching data. * Review changes Co-authored-by: Martin Hjelmare * Review changes Co-authored-by: Martin Hjelmare * Review changes Co-authored-by: Martin Hjelmare * Review changes Co-authored-by: Martin Hjelmare * Review changes Co-authored-by: Martin Hjelmare * Review changes Co-authored-by: Martin Hjelmare * Review changes Co-authored-by: Martin Hjelmare * Review changes Co-authored-by: Martin Hjelmare * Review changes Co-authored-by: Martin Hjelmare * Review changes Co-authored-by: Martin Hjelmare * Update review suggestions * Rremove custom play action script. * Add test requirements. * Tidy manifest. * Missed fixes. * Refactor config_flow to use current pattern. * Add config_flow tests. * Refactor to use signal dispatch helpers. * Remove ToDo: for now. * Remove remaining zone / source logic for initial release, * Stop authenticate blocking, handle timeout. * Removed unneeded code. * Review comments update. * Fix comment. * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Fix bug in seek. * Use sync rather than async update Co-authored-by: Martin Hjelmare * Upgrade library, remove exception now caught in library, * Review comments. * Review changes Co-authored-by: Martin Hjelmare * Check for duplicate host before adding. * Review comment. Co-authored-by: Martin Hjelmare * Remove unused code, revise turn_on/turn_off. * Sync translations. * Make interim timeout const. * Refactor tests. * Add tests with an existing config entry. * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Remove CannotConnect Co-authored-by: Martin Hjelmare --- .coveragerc | 4 + CODEOWNERS | 1 + homeassistant/components/roon/__init__.py | 41 ++ homeassistant/components/roon/config_flow.py | 108 +++++ homeassistant/components/roon/const.py | 18 + homeassistant/components/roon/manifest.json | 12 + homeassistant/components/roon/media_player.py | 431 ++++++++++++++++++ homeassistant/components/roon/server.py | 153 +++++++ homeassistant/components/roon/strings.json | 26 ++ .../components/roon/translations/en.json | 26 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/roon/__init__.py | 1 + tests/components/roon/test_config_flow.py | 159 +++++++ 15 files changed, 987 insertions(+) create mode 100644 homeassistant/components/roon/__init__.py create mode 100644 homeassistant/components/roon/config_flow.py create mode 100644 homeassistant/components/roon/const.py create mode 100644 homeassistant/components/roon/manifest.json create mode 100644 homeassistant/components/roon/media_player.py create mode 100644 homeassistant/components/roon/server.py create mode 100644 homeassistant/components/roon/strings.json create mode 100644 homeassistant/components/roon/translations/en.json create mode 100644 tests/components/roon/__init__.py create mode 100644 tests/components/roon/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 6809a58f157..e6fcfea3543 100644 --- a/.coveragerc +++ b/.coveragerc @@ -718,6 +718,10 @@ omit = homeassistant/components/roomba/roomba.py homeassistant/components/roomba/sensor.py homeassistant/components/roomba/vacuum.py + homeassistant/components/roon/__init__.py + homeassistant/components/roon/const.py + homeassistant/components/roon/media_player.py + homeassistant/components/roon/server.py homeassistant/components/route53/* homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* diff --git a/CODEOWNERS b/CODEOWNERS index a92eac1940d..768c99cb698 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -348,6 +348,7 @@ homeassistant/components/ring/* @balloob homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roku/* @ctalkington homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn +homeassistant/components/roon/* @pavoni homeassistant/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl homeassistant/components/salt/* @bjornorri diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py new file mode 100644 index 00000000000..ff45e2c16cf --- /dev/null +++ b/homeassistant/components/roon/__init__.py @@ -0,0 +1,41 @@ +"""Roon (www.roonlabs.com) component.""" +import logging + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import device_registry as dr + +from .const import DOMAIN +from .server import RoonServer + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the Roon platform.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass, entry): + """Set up a roonserver from a config entry.""" + host = entry.data[CONF_HOST] + roonserver = RoonServer(hass, entry) + + if not await roonserver.async_setup(): + return False + + hass.data[DOMAIN][entry.entry_id] = roonserver + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="Roonlabs", + name=host, + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + roonserver = hass.data[DOMAIN].pop(entry.entry_id) + return await roonserver.async_reset() diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py new file mode 100644 index 00000000000..7fac3186c03 --- /dev/null +++ b/homeassistant/components/roon/config_flow.py @@ -0,0 +1,108 @@ +"""Config flow for roon integration.""" +import asyncio +import logging + +from roon import RoonApi +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_KEY, CONF_HOST + +from .const import ( # pylint: disable=unused-import + AUTHENTICATE_TIMEOUT, + DEFAULT_NAME, + DOMAIN, + ROON_APPINFO, +) + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({"host": str}) + +TIMEOUT = 120 + + +class RoonHub: + """Interact with roon during config flow.""" + + def __init__(self, host): + """Initialize.""" + self._host = host + + async def authenticate(self, hass) -> bool: + """Test if we can authenticate with the host.""" + token = None + secs = 0 + roonapi = RoonApi(ROON_APPINFO, None, self._host, blocking_init=False) + while secs < TIMEOUT: + token = roonapi.token + secs += AUTHENTICATE_TIMEOUT + if token: + break + await asyncio.sleep(AUTHENTICATE_TIMEOUT) + + token = roonapi.token + roonapi.stop() + return token + + +async def authenticate(hass: core.HomeAssistant, host): + """Connect and authenticate home assistant.""" + + hub = RoonHub(host) + token = await hub.authenticate(hass) + if token is None: + raise InvalidAuth + + return {CONF_HOST: host, CONF_API_KEY: token} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for roon.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the Roon flow.""" + self._host = None + + async def async_step_user(self, user_input=None): + """Handle getting host details from the user.""" + + errors = {} + if user_input is not None: + self._host = user_input["host"] + existing = { + entry.data[CONF_HOST] for entry in self._async_current_entries() + } + if self._host in existing: + errors["base"] = "duplicate_entry" + return self.async_show_form(step_id="user", errors=errors) + + return await self.async_step_link() + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_link(self, user_input=None): + """Handle linking and authenticting with the roon server.""" + + errors = {} + if user_input is not None: + try: + info = await authenticate(self.hass, self._host) + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=DEFAULT_NAME, data=info) + + return self.async_show_form(step_id="link", errors=errors) + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/roon/const.py b/homeassistant/components/roon/const.py new file mode 100644 index 00000000000..dc11d4167a7 --- /dev/null +++ b/homeassistant/components/roon/const.py @@ -0,0 +1,18 @@ +"""Constants for Roon Component.""" + +AUTHENTICATE_TIMEOUT = 5 + +DOMAIN = "roon" + +DATA_CONFIGS = "roon_configs" + +DEFAULT_NAME = "Roon Labs Music Player" + +ROON_APPINFO = { + "extension_id": "home_assistant", + "display_name": "Roon Integration for Home Assistant", + "display_version": "1.0.0", + "publisher": "home_assistant", + "email": "home_assistant@users.noreply.github.com", + "website": "https://www.home-assistant.io/", +} diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json new file mode 100644 index 00000000000..d6bf70e427b --- /dev/null +++ b/homeassistant/components/roon/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "roon", + "name": "RoonLabs music player", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/roon", + "requirements": [ + "roonapi==0.0.21" + ], + "codeowners": [ + "@pavoni" + ] +} diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py new file mode 100644 index 00000000000..c4aa3dc8a2c --- /dev/null +++ b/homeassistant/components/roon/media_player.py @@ -0,0 +1,431 @@ +"""MediaPlayer platform for Roon integration.""" +import logging + +from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.const import ( + DEVICE_DEFAULT_NAME, + STATE_IDLE, + STATE_OFF, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.util import convert +from homeassistant.util.dt import utcnow + +from .const import DOMAIN + +SUPPORT_ROON = ( + SUPPORT_PAUSE + | SUPPORT_VOLUME_SET + | SUPPORT_STOP + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_NEXT_TRACK + | SUPPORT_SHUFFLE_SET + | SUPPORT_SEEK + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_VOLUME_MUTE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_VOLUME_STEP +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Roon MediaPlayer from Config Entry.""" + roon_server = hass.data[DOMAIN][config_entry.entry_id] + media_players = set() + + @callback + def async_update_media_player(player_data): + """Add or update Roon MediaPlayer.""" + dev_id = player_data["dev_id"] + if dev_id not in media_players: + # new player! + media_player = RoonDevice(roon_server, player_data) + media_players.add(dev_id) + async_add_entities([media_player]) + else: + # update existing player + async_dispatcher_send( + hass, f"room_media_player_update_{dev_id}", player_data + ) + + # start listening for players to be added or changed by the server component + async_dispatcher_connect(hass, "roon_media_player", async_update_media_player) + + +class RoonDevice(MediaPlayerEntity): + """Representation of an Roon device.""" + + def __init__(self, server, player_data): + """Initialize Roon device object.""" + self._remove_signal_status = None + self._server = server + self._available = True + self._last_position_update = None + self._supports_standby = False + self._state = STATE_IDLE + self._last_playlist = None + self._last_media = None + self._unique_id = None + self._zone_id = None + self._output_id = None + self._name = DEVICE_DEFAULT_NAME + self._media_title = None + self._media_album_name = None + self._media_artist = None + self._media_position = 0 + self._media_duration = 0 + self._is_volume_muted = False + self._volume_step = 0 + self._shuffle = False + self._media_image_url = None + self._volume_level = 0 + self.update_data(player_data) + + async def async_added_to_hass(self): + """Register callback.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"room_media_player_update_{self.unique_id}", + self.async_update_callback, + ) + ) + + @callback + def async_update_callback(self, player_data): + """Handle device updates.""" + self.update_data(player_data) + self.async_write_ha_state() + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_ROON + + @property + def device_info(self): + """Return the device info.""" + dev_model = "player" + if self.player_data.get("source_controls"): + dev_model = self.player_data["source_controls"][0].get("display_name") + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "RoonLabs", + "model": dev_model, + "via_hub": (DOMAIN, self._server.host), + } + + def update_data(self, player_data=None): + """Update session object.""" + if player_data: + self.player_data = player_data + if not self.player_data["is_available"]: + # this player was removed + self._available = False + self._state = STATE_OFF + else: + self._available = True + # determine player state + self.update_state() + if self.state == STATE_PLAYING: + self._last_position_update = utcnow() + + def update_state(self): + """Update the power state and player state.""" + new_state = "" + # power state from source control (if supported) + if "source_controls" in self.player_data: + for source in self.player_data["source_controls"]: + if source["supports_standby"] and source["status"] != "indeterminate": + self._supports_standby = True + if source["status"] in ["standby", "deselected"]: + new_state = STATE_OFF + break + # determine player state + if not new_state: + if self.player_data["state"] == "playing": + new_state = STATE_PLAYING + elif self.player_data["state"] == "loading": + new_state = STATE_PLAYING + elif self.player_data["state"] == "stopped": + new_state = STATE_IDLE + elif self.player_data["state"] == "paused": + new_state = STATE_PAUSED + else: + new_state = STATE_IDLE + self._state = new_state + self._unique_id = self.player_data["dev_id"] + self._zone_id = self.player_data["zone_id"] + self._output_id = self.player_data["output_id"] + self._name = self.player_data["display_name"] + self._is_volume_muted = self.player_data["volume"]["is_muted"] + self._volume_step = convert(self.player_data["volume"]["step"], int, 0) + self._shuffle = self.player_data["settings"]["shuffle"] + + if self.player_data["volume"]["type"] == "db": + volume = ( + convert(self.player_data["volume"]["value"], float, 0.0) / 80 * 100 + + 100 + ) + else: + volume = convert(self.player_data["volume"]["value"], float, 0.0) + self._volume_level = convert(volume, int, 0) / 100 + + try: + self._media_title = self.player_data["now_playing"]["three_line"]["line1"] + self._media_artist = self.player_data["now_playing"]["three_line"]["line2"] + self._media_album_name = self.player_data["now_playing"]["three_line"][ + "line3" + ] + self._media_position = convert( + self.player_data["now_playing"]["seek_position"], int, 0 + ) + self._media_duration = convert( + self.player_data["now_playing"]["length"], int, 0 + ) + try: + image_id = self.player_data["now_playing"]["image_key"] + self._media_image_url = self._server.roonapi.get_image(image_id) + except KeyError: + self._media_image_url = None + except KeyError: + self._media_title = None + self._media_album_name = None + self._media_artist = None + self._media_position = 0 + self._media_duration = 0 + self._media_image_url = None + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid.""" + # Returns value from homeassistant.util.dt.utcnow(). + return self._last_position_update + + @property + def unique_id(self): + """Return the id of this roon client.""" + return self._unique_id + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + @property + def zone_id(self): + """Return current session Id.""" + return self._zone_id + + @property + def output_id(self): + """Return current session Id.""" + return self._output_id + + @property + def name(self): + """Return device name.""" + return self._name + + @property + def media_title(self): + """Return title currently playing.""" + return self._media_title + + @property + def media_album_name(self): + """Album name of current playing media (Music track only).""" + return self._media_album_name + + @property + def media_artist(self): + """Artist of current playing media (Music track only).""" + return self._media_artist + + @property + def media_album_artist(self): + """Album artist of current playing media (Music track only).""" + return self._media_artist + + @property + def media_playlist(self): + """Title of Playlist currently playing.""" + return self._last_playlist + + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._media_image_url + + @property + def media_position(self): + """Return position currently playing.""" + return self._media_position + + @property + def media_duration(self): + """Return total runtime length.""" + return self._media_duration + + @property + def volume_level(self): + """Return current volume level.""" + return self._volume_level + + @property + def is_volume_muted(self): + """Return mute state.""" + return self._is_volume_muted + + @property + def volume_step(self): + """.Return volume step size.""" + return self._volume_step + + @property + def supports_standby(self): + """Return power state of source controls.""" + return self._supports_standby + + @property + def state(self): + """Return current playstate of the device.""" + return self._state + + @property + def shuffle(self): + """Boolean if shuffle is enabled.""" + return self._shuffle + + def media_play(self): + """Send play command to device.""" + self._server.roonapi.playback_control(self.output_id, "play") + + def media_pause(self): + """Send pause command to device.""" + self._server.roonapi.playback_control(self.output_id, "pause") + + def media_play_pause(self): + """Toggle play command to device.""" + self._server.roonapi.playback_control(self.output_id, "playpause") + + def media_stop(self): + """Send stop command to device.""" + self._server.roonapi.playback_control(self.output_id, "stop") + + def media_next_track(self): + """Send next track command to device.""" + self._server.roonapi.playback_control(self.output_id, "next") + + def media_previous_track(self): + """Send previous track command to device.""" + self._server.roonapi.playback_control(self.output_id, "previous") + + def media_seek(self, position): + """Send seek command to device.""" + self._server.roonapi.seek(self.output_id, position) + # Seek doesn't cause an async update - so force one + self._media_position = position + self.schedule_update_ha_state() + + def set_volume_level(self, volume): + """Send new volume_level to device.""" + volume = int(volume * 100) + self._server.roonapi.change_volume(self.output_id, volume) + + def mute_volume(self, mute=True): + """Send mute/unmute to device.""" + self._server.roonapi.mute(self.output_id, mute) + + def volume_up(self): + """Send new volume_level to device.""" + self._server.roonapi.change_volume(self.output_id, 3, "relative") + + def volume_down(self): + """Send new volume_level to device.""" + self._server.roonapi.change_volume(self.output_id, -3, "relative") + + def turn_on(self): + """Turn on device (if supported).""" + if not (self.supports_standby and "source_controls" in self.player_data): + self.media_play() + return + for source in self.player_data["source_controls"]: + if source["supports_standby"] and source["status"] != "indeterminate": + self._server.roonapi.convenience_switch( + self.output_id, source["control_key"] + ) + return + + def turn_off(self): + """Turn off device (if supported).""" + if not (self.supports_standby and "source_controls" in self.player_data): + self.media_stop() + return + + for source in self.player_data["source_controls"]: + if source["supports_standby"] and not source["status"] == "indeterminate": + self._server.roonapi.standby(self.output_id, source["control_key"]) + return + + def set_shuffle(self, shuffle): + """Set shuffle state.""" + self._server.roonapi.shuffle(self.output_id, shuffle) + + def play_media(self, media_type, media_id, **kwargs): + """Send the play_media command to the media player.""" + # Roon itself doesn't support playback of media by filename/url so this a bit of a workaround. + media_type = media_type.lower() + if media_type == "radio": + if self._server.roonapi.play_radio(self.zone_id, media_id): + self._last_playlist = media_id + self._last_media = media_id + elif media_type == "playlist": + if self._server.roonapi.play_playlist( + self.zone_id, media_id, shuffle=False + ): + self._last_playlist = media_id + elif media_type == "shuffleplaylist": + if self._server.roonapi.play_playlist(self.zone_id, media_id, shuffle=True): + self._last_playlist = media_id + elif media_type == "queueplaylist": + self._server.roonapi.queue_playlist(self.zone_id, media_id) + elif media_type == "genre": + self._server.roonapi.play_genre(self.zone_id, media_id) + else: + _LOGGER.error( + "Playback requested of unsupported type: %s --> %s", + media_type, + media_id, + ) diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py new file mode 100644 index 00000000000..df6051c287a --- /dev/null +++ b/homeassistant/components/roon/server.py @@ -0,0 +1,153 @@ +"""Code to handle the api connection to a Roon server.""" +import asyncio +import logging + +from roon import RoonApi + +from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util.dt import utcnow + +from .const import ROON_APPINFO + +_LOGGER = logging.getLogger(__name__) +FULL_SYNC_INTERVAL = 30 + + +class RoonServer: + """Manages a single Roon Server.""" + + def __init__(self, hass, config_entry): + """Initialize the system.""" + self.config_entry = config_entry + self.hass = hass + self.roonapi = None + self.all_player_ids = set() + self.all_playlists = [] + self.offline_devices = set() + self._exit = False + + @property + def host(self): + """Return the host of this server.""" + return self.config_entry.data[CONF_HOST] + + async def async_setup(self, tries=0): + """Set up a roon server based on host parameter.""" + host = self.host + hass = self.hass + token = self.config_entry.data[CONF_API_KEY] + _LOGGER.debug("async_setup: %s %s", token, host) + self.roonapi = RoonApi(ROON_APPINFO, token, host, blocking_init=False) + self.roonapi.register_state_callback( + self.roonapi_state_callback, event_filter=["zones_changed"] + ) + + # initialize media_player platform + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + self.config_entry, "media_player" + ) + ) + + # Initialize Roon background polling + asyncio.create_task(self.async_do_loop()) + + return True + + async def async_reset(self): + """Reset this connection to default state. + + Will cancel any scheduled setup retry and will unload + the config entry. + """ + self.stop_roon() + return True + + @property + def zones(self): + """Return list of zones.""" + return self.roonapi.zones + + def stop_roon(self): + """Stop background worker.""" + self.roonapi.stop() + self._exit = True + + def roonapi_state_callback(self, event, changed_zones): + """Callbacks from the roon api websockets.""" + self.hass.add_job(self.async_update_changed_players(changed_zones)) + + async def async_do_loop(self): + """Background work loop.""" + self._exit = False + while not self._exit: + await self.async_update_players() + # await self.async_update_playlists() + await asyncio.sleep(FULL_SYNC_INTERVAL) + + async def async_update_changed_players(self, changed_zones_ids): + """Update the players which were reported as changed by the Roon API.""" + for zone_id in changed_zones_ids: + if zone_id not in self.roonapi.zones: + # device was removed ? + continue + zone = self.roonapi.zones[zone_id] + for device in zone["outputs"]: + dev_name = device["display_name"] + if dev_name == "Unnamed" or not dev_name: + # ignore unnamed devices + continue + player_data = await self.async_create_player_data(zone, device) + dev_id = player_data["dev_id"] + player_data["is_available"] = True + if dev_id in self.offline_devices: + # player back online + self.offline_devices.remove(dev_id) + async_dispatcher_send(self.hass, "roon_media_player", player_data) + self.all_player_ids.add(dev_id) + + async def async_update_players(self): + """Periodic full scan of all devices.""" + zone_ids = self.roonapi.zones.keys() + await self.async_update_changed_players(zone_ids) + # check for any removed devices + all_devs = {} + for zone in self.roonapi.zones.values(): + for device in zone["outputs"]: + player_data = await self.async_create_player_data(zone, device) + dev_id = player_data["dev_id"] + all_devs[dev_id] = player_data + for dev_id in self.all_player_ids: + if dev_id in all_devs: + continue + # player was removed! + player_data = {"dev_id": dev_id} + player_data["is_available"] = False + async_dispatcher_send(self.hass, "roon_media_player", player_data) + self.offline_devices.add(dev_id) + + async def async_update_playlists(self): + """Store lists in memory with all playlists - could be used by a custom lovelace card.""" + all_playlists = [] + roon_playlists = self.roonapi.playlists() + if roon_playlists and "items" in roon_playlists: + all_playlists += [item["title"] for item in roon_playlists["items"]] + roon_playlists = self.roonapi.internet_radio() + if roon_playlists and "items" in roon_playlists: + all_playlists += [item["title"] for item in roon_playlists["items"]] + self.all_playlists = all_playlists + + async def async_create_player_data(self, zone, output): + """Create player object dict by combining zone with output.""" + new_dict = zone.copy() + new_dict.update(output) + new_dict.pop("outputs") + new_dict["host"] = self.host + new_dict["is_synced"] = len(zone["outputs"]) > 1 + new_dict["zone_name"] = zone["display_name"] + new_dict["display_name"] = output["display_name"] + new_dict["last_changed"] = utcnow() + # we don't use the zone_id or output_id for now as unique id as I've seen cases were it changes for some reason + new_dict["dev_id"] = f"roon_{self.host}_{output['display_name']}" + return new_dict diff --git a/homeassistant/components/roon/strings.json b/homeassistant/components/roon/strings.json new file mode 100644 index 00000000000..741fc01605f --- /dev/null +++ b/homeassistant/components/roon/strings.json @@ -0,0 +1,26 @@ +{ + "title": "Roon", + "config": { + "step": { + "user": { + "title": "Configure Roon Server", + "description": "Please enter your Roon server Hostname or IP.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "link": { + "title": "Authorize HomeAssistant in Roon", + "description": "You must authorize Home Assistant in Roon. After you click submit, go to the Roon Core application, open Settings and enable HomeAssistant on the Extensions tab." + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "duplicate_entry": "That host has already been added." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/roon/translations/en.json b/homeassistant/components/roon/translations/en.json new file mode 100644 index 00000000000..230fe87809a --- /dev/null +++ b/homeassistant/components/roon/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "duplicate_entry": "That host has already been added.", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "link": { + "description": "You must authorize Home Assistant in Roon. After you click submit, go to the Roon Core application, open Settings and enable HomeAssistant on the Extensions tab.", + "title": "Authorize HomeAssistant in Roon" + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "description": "Please enter your Roon server Hostname or IP.", + "title": "Configure Roon Server" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9b88a7bda0f..4a4548571ec 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -144,6 +144,7 @@ FLOWS = [ "ring", "roku", "roomba", + "roon", "samsungtv", "sense", "sentry", diff --git a/requirements_all.txt b/requirements_all.txt index 9f538fb497b..810fd65fab5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1914,6 +1914,9 @@ rokuecp==0.5.0 # homeassistant.components.roomba roombapy==1.6.1 +# homeassistant.components.roon +roonapi==0.0.21 + # homeassistant.components.rova rova==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e766138092..ee2f251428e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -871,6 +871,9 @@ rokuecp==0.5.0 # homeassistant.components.roomba roombapy==1.6.1 +# homeassistant.components.roon +roonapi==0.0.21 + # homeassistant.components.yamaha rxv==0.6.0 diff --git a/tests/components/roon/__init__.py b/tests/components/roon/__init__.py new file mode 100644 index 00000000000..4babd3f177e --- /dev/null +++ b/tests/components/roon/__init__.py @@ -0,0 +1 @@ +"""Tests for the roon integration.""" diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py new file mode 100644 index 00000000000..8b6df6a35fd --- /dev/null +++ b/tests/components/roon/test_config_flow.py @@ -0,0 +1,159 @@ +"""Test the roon config flow.""" +from homeassistant import config_entries, setup +from homeassistant.components.roon.const import DOMAIN +from homeassistant.const import CONF_HOST + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +class RoonApiMock: + """Mock to handle returning tokens for testing the RoonApi.""" + + def __init__(self, token): + """Initialize.""" + self._token = token + + @property + def token(self): + """Return the auth token from the api.""" + return self._token + + def stop(self): # pylint: disable=no-self-use + """Close down the api.""" + return + + +async def test_form_and_auth(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.roon.config_flow.TIMEOUT", 0,), patch( + "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", 0, + ), patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMock("good_token"), + ), patch( + "homeassistant.components.roon.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roon.async_setup_entry", return_value=True, + ) as mock_setup_entry: + await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1"} + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Roon Labs Music Player" + assert result2["data"] == {"host": "1.1.1.1", "api_key": "good_token"} + 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_no_token(hass): + """Test we handle no token being returned (timeout or not authorized).""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch("homeassistant.components.roon.config_flow.TIMEOUT", 0,), patch( + "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", 0, + ), patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMock(None), + ): + await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1"} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_unknown_exception(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.roon.config_flow.RoonApi", side_effect=Exception, + ): + await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1"} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_host_already_exists(hass): + """Test we add the host if the config exists and it isn't a duplicate.""" + + MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "existing_host"}).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} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch("homeassistant.components.roon.config_flow.TIMEOUT", 0,), patch( + "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", 0, + ), patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMock("good_token"), + ), patch( + "homeassistant.components.roon.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roon.async_setup_entry", return_value=True, + ) as mock_setup_entry: + await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1"} + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Roon Labs Music Player" + assert result2["data"] == {"host": "1.1.1.1", "api_key": "good_token"} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 + + +async def test_form_duplicate_host(hass): + """Test we don't add the host if it's a duplicate.""" + + MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "existing_host"}).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} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "existing_host"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "duplicate_entry"} From 7949357180384a757ce035e4b472f842ebdb521e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 12 Aug 2020 15:17:21 +0200 Subject: [PATCH 125/862] Unsubscribe ozw listeners (#38787) --- homeassistant/components/ozw/__init__.py | 17 ++++++++++------- homeassistant/components/ozw/entity.py | 20 ++++++++------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index bbac0e843e9..ae79850d96f 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -193,13 +193,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ] # Listen to events for node and value changes - options.listen(EVENT_NODE_ADDED, async_node_added) - options.listen(EVENT_NODE_CHANGED, async_node_changed) - options.listen(EVENT_NODE_REMOVED, async_node_removed) - options.listen(EVENT_VALUE_ADDED, async_value_added) - options.listen(EVENT_VALUE_CHANGED, async_value_changed) - options.listen(EVENT_VALUE_REMOVED, async_value_removed) - options.listen(EVENT_INSTANCE_EVENT, async_instance_event) + for event, event_callback in ( + (EVENT_NODE_ADDED, async_node_added), + (EVENT_NODE_CHANGED, async_node_changed), + (EVENT_NODE_REMOVED, async_node_removed), + (EVENT_VALUE_ADDED, async_value_added), + (EVENT_VALUE_CHANGED, async_value_changed), + (EVENT_VALUE_REMOVED, async_value_removed), + (EVENT_INSTANCE_EVENT, async_instance_event), + ): + ozw_data[DATA_UNSUBSCRIBE].append(options.listen(event, event_callback)) # Register Services services = ZWaveServices(hass, manager) diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py index 39971d0c976..9c494a514e0 100644 --- a/homeassistant/components/ozw/entity.py +++ b/homeassistant/components/ozw/entity.py @@ -162,10 +162,14 @@ class ZWaveDeviceEntity(Entity): async def async_added_to_hass(self): """Call when entity is added.""" - # add dispatcher and OZW listeners callbacks, - self.options.listen(EVENT_VALUE_CHANGED, self._value_changed) - self.options.listen(EVENT_INSTANCE_STATUS_CHANGED, self._instance_updated) - # add to on_remove so they will be cleaned up on entity removal + # Add dispatcher and OZW listeners callbacks. + # Add to on_remove so they will be cleaned up on entity removal. + self.async_on_remove( + self.options.listen(EVENT_VALUE_CHANGED, self._value_changed) + ) + self.async_on_remove( + self.options.listen(EVENT_INSTANCE_STATUS_CHANGED, self._instance_updated) + ) self.async_on_remove( async_dispatcher_connect( self.hass, const.SIGNAL_DELETE_ENTITY, self._delete_callback @@ -266,14 +270,6 @@ class ZWaveDeviceEntity(Entity): if values_id == self.values.values_id: await self.async_remove() - async def async_will_remove_from_hass(self) -> None: - """Call when entity will be removed from hass.""" - # cleanup OZW listeners - self.options.listeners[EVENT_VALUE_CHANGED].remove(self._value_changed) - self.options.listeners[EVENT_INSTANCE_STATUS_CHANGED].remove( - self._instance_updated - ) - def create_device_name(node: OZWNode): """Generate sensible (short) default device name from a OZWNode.""" From d058802325598ab44e319f65525045295781e366 Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Wed, 12 Aug 2020 16:18:26 +0300 Subject: [PATCH 126/862] Add dynalite level preset (#37533) * implementation of "level" in preset * updated library version - bug fix for covers during init with active=on * cleanup after merge --- homeassistant/components/dynalite/__init__.py | 7 ++++++- homeassistant/components/dynalite/const.py | 1 + homeassistant/components/dynalite/convert_config.py | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index dd485af0441..c131ebec3da 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -33,6 +33,7 @@ from .const import ( CONF_DEVICE_CLASS, CONF_DURATION, CONF_FADE, + CONF_LEVEL, CONF_NO_DEFAULT, CONF_OPEN_PRESET, CONF_POLL_TIMER, @@ -75,7 +76,11 @@ CHANNEL_DATA_SCHEMA = vol.Schema( CHANNEL_SCHEMA = vol.Schema({num_string: CHANNEL_DATA_SCHEMA}) PRESET_DATA_SCHEMA = vol.Schema( - {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_FADE): vol.Coerce(float)} + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_FADE): vol.Coerce(float), + vol.Optional(CONF_LEVEL): vol.Coerce(float), + } ) PRESET_SCHEMA = vol.Schema({num_string: vol.Any(PRESET_DATA_SCHEMA, None)}) diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py index cfe48bdc475..4159c98f073 100644 --- a/homeassistant/components/dynalite/const.py +++ b/homeassistant/components/dynalite/const.py @@ -23,6 +23,7 @@ CONF_DEFAULT = "default" CONF_DEVICE_CLASS = "class" CONF_DURATION = "duration" CONF_FADE = "fade" +CONF_LEVEL = "level" CONF_NO_DEFAULT = "nodefault" CONF_OPEN_PRESET = "open" CONF_POLL_TIMER = "polltimer" diff --git a/homeassistant/components/dynalite/convert_config.py b/homeassistant/components/dynalite/convert_config.py index 3cc9372eb0b..b84450c807d 100644 --- a/homeassistant/components/dynalite/convert_config.py +++ b/homeassistant/components/dynalite/convert_config.py @@ -20,6 +20,7 @@ from .const import ( CONF_DEVICE_CLASS, CONF_DURATION, CONF_FADE, + CONF_LEVEL, CONF_NO_DEFAULT, CONF_OPEN_PRESET, CONF_POLL_TIMER, @@ -70,6 +71,7 @@ def convert_preset(config: Dict[str, Any]) -> Dict[str, Any]: my_map = { CONF_NAME: dyn_const.CONF_NAME, CONF_FADE: dyn_const.CONF_FADE, + CONF_LEVEL: dyn_const.CONF_LEVEL, } return convert_with_map(config, my_map) From 34cb12d3c92dd90d8aebe19cd3ef143554030d9e Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Wed, 12 Aug 2020 06:34:27 -0700 Subject: [PATCH 127/862] Addressing feedback from #37711 (#38781) --- homeassistant/components/onvif/event.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 59076fd938d..514e7594370 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -12,7 +12,7 @@ from onvif import ONVIFCamera, ONVIFService from zeep.exceptions import Fault from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.event import async_call_later from homeassistant.util import dt as dt_util from .const import LOGGER @@ -56,7 +56,7 @@ class EventManager: """Listen for data updates.""" # This is the first listener, set up polling. if not self._listeners: - self.schedule_pull() + self.async_schedule_pull() self._listeners.append(update_callback) @@ -135,16 +135,12 @@ class EventManager: self.unique_id, ) # Try again in a minute - self._unsub_refresh = async_track_point_in_utc_time( - self.hass, - self.async_restart, - dt_util.utcnow() + dt.timedelta(seconds=60), - ) + self._unsub_refresh = async_call_later(self.hass, 60, self.async_restart) elif self._listeners: - LOGGER.info( + LOGGER.debug( "Restarted ONVIF PullPoint subscription for '%s'", self.unique_id ) - self.schedule_pull() + self.async_schedule_pull() async def async_renew(self) -> None: """Renew subscription.""" @@ -158,13 +154,9 @@ class EventManager: ) await self._subscription.Renew(termination_time) - def schedule_pull(self) -> None: + def async_schedule_pull(self) -> None: """Schedule async_pull_messages to run.""" - self._unsub_refresh = async_track_point_in_utc_time( - self.hass, - self.async_pull_messages, - dt_util.utcnow() + dt.timedelta(seconds=1), - ) + self._unsub_refresh = async_call_later(self.hass, 1, self.async_pull_messages) async def async_pull_messages(self, _now: dt = None) -> None: """Pull messages from device.""" @@ -201,7 +193,7 @@ class EventManager: # Reschedule another pull if self._listeners: - self.schedule_pull() + self.async_schedule_pull() # pylint: disable=protected-access async def async_parse_messages(self, messages) -> None: From 444df4a7d2d20edfbe4ff8fe0ba10413d5b7205e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Aug 2020 09:08:33 -0500 Subject: [PATCH 128/862] Use the shared zeroconf instance when attempting to create another Zeroconf instance (#38744) --- homeassistant/components/zeroconf/__init__.py | 4 ++ homeassistant/components/zeroconf/usage.py | 50 +++++++++++++++++ homeassistant/helpers/frame.py | 35 +++++++++--- tests/components/conftest.py | 7 +++ tests/components/zeroconf/conftest.py | 11 ++++ tests/components/zeroconf/test_init.py | 8 --- tests/components/zeroconf/test_usage.py | 56 +++++++++++++++++++ tests/helpers/test_frame.py | 33 +++++++++++ 8 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/zeroconf/usage.py create mode 100644 tests/components/zeroconf/conftest.py create mode 100644 tests/components/zeroconf/test_usage.py diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 71e2f67bad7..5bbc87f3da8 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -30,6 +30,8 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.singleton import singleton from homeassistant.loader import async_get_homekit, async_get_zeroconf +from .usage import install_multiple_zeroconf_catcher + _LOGGER = logging.getLogger(__name__) DOMAIN = "zeroconf" @@ -135,6 +137,8 @@ def setup(hass, config): ipv6=zc_config.get(CONF_IPV6, DEFAULT_IPV6), ) + install_multiple_zeroconf_catcher(zeroconf) + # Get instance UUID uuid = asyncio.run_coroutine_threadsafe( hass.helpers.instance_id.async_get(), hass.loop diff --git a/homeassistant/components/zeroconf/usage.py b/homeassistant/components/zeroconf/usage.py new file mode 100644 index 00000000000..1303412249c --- /dev/null +++ b/homeassistant/components/zeroconf/usage.py @@ -0,0 +1,50 @@ +"""Zeroconf usage utility to warn about multiple instances.""" + +import logging + +import zeroconf + +from homeassistant.helpers.frame import ( + MissingIntegrationFrame, + get_integration_frame, + report_integration, +) + +_LOGGER = logging.getLogger(__name__) + + +def install_multiple_zeroconf_catcher(hass_zc) -> None: + """Wrap the Zeroconf class to return the shared instance if multiple instances are detected.""" + + def new_zeroconf_new(self, *k, **kw): + _report( + "attempted to create another Zeroconf instance. Please use the shared Zeroconf via await homeassistant.components.zeroconf.async_get_instance(hass)", + ) + return hass_zc + + def new_zeroconf_init(self, *k, **kw): + return + + zeroconf.Zeroconf.__new__ = new_zeroconf_new + zeroconf.Zeroconf.__init__ = new_zeroconf_init + + +def _report(what: str) -> None: + """Report incorrect usage. + + Async friendly. + """ + integration_frame = None + + try: + integration_frame = get_integration_frame(exclude_integrations={"zeroconf"}) + except MissingIntegrationFrame: + pass + + if not integration_frame: + _LOGGER.warning( + "Detected code that %s. Please report this issue.", what, stack_info=True + ) + return + + report_integration(what, integration_frame) diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 63d7cba4ec5..35f7b3fab9f 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -3,7 +3,7 @@ import asyncio import functools import logging from traceback import FrameSummary, extract_stack -from typing import Any, Callable, Tuple, TypeVar, cast +from typing import Any, Callable, Optional, Tuple, TypeVar, cast from homeassistant.exceptions import HomeAssistantError @@ -12,15 +12,24 @@ _LOGGER = logging.getLogger(__name__) CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name -def get_integration_frame() -> Tuple[FrameSummary, str, str]: +def get_integration_frame( + exclude_integrations: Optional[set] = None, +) -> Tuple[FrameSummary, str, str]: """Return the frame, integration and integration path of the current stack frame.""" found_frame = None + if not exclude_integrations: + exclude_integrations = set() for frame in reversed(extract_stack()): for path in ("custom_components/", "homeassistant/components/"): try: index = frame.filename.index(path) - found_frame = frame + start = index + len(path) + end = frame.filename.index("/", start) + integration = frame.filename[start:end] + if integration not in exclude_integrations: + found_frame = frame + break except ValueError: continue @@ -31,11 +40,6 @@ def get_integration_frame() -> Tuple[FrameSummary, str, str]: if found_frame is None: raise MissingIntegrationFrame - start = index + len(path) - end = found_frame.filename.index("/", start) - - integration = found_frame.filename[start:end] - return found_frame, integration, path @@ -49,11 +53,24 @@ def report(what: str) -> None: Async friendly. """ try: - found_frame, integration, path = get_integration_frame() + integration_frame = get_integration_frame() except MissingIntegrationFrame: # Did not source from an integration? Hard error. raise RuntimeError(f"Detected code that {what}. Please report this issue.") + report_integration(what, integration_frame) + + +def report_integration( + what: str, integration_frame: Tuple[FrameSummary, str, str] +) -> None: + """Report incorrect usage in an integration. + + Async friendly. + """ + + found_frame, integration, path = integration_frame + index = found_frame.filename.index(path) if path == "custom_components/": extra = " to the custom component author" diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 96ab3bca543..e78a67a16f3 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,8 +1,15 @@ """Fixtures for component testing.""" import pytest +from homeassistant.components import zeroconf + from tests.async_mock import patch +zeroconf.orig_install_multiple_zeroconf_catcher = ( + zeroconf.install_multiple_zeroconf_catcher +) +zeroconf.install_multiple_zeroconf_catcher = lambda zc: None + @pytest.fixture(autouse=True) def prevent_io(): diff --git a/tests/components/zeroconf/conftest.py b/tests/components/zeroconf/conftest.py new file mode 100644 index 00000000000..e7d7b030aaf --- /dev/null +++ b/tests/components/zeroconf/conftest.py @@ -0,0 +1,11 @@ +"""conftest for zeroconf.""" +import pytest + +from tests.async_mock import patch + + +@pytest.fixture +def mock_zeroconf(): + """Mock zeroconf.""" + with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: + yield mock_zc.return_value diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index e8315b5dc75..2ced2fac8ea 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,5 +1,4 @@ """Test Zeroconf component setup process.""" -import pytest from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange from homeassistant.components import zeroconf @@ -22,13 +21,6 @@ HOMEKIT_STATUS_UNPAIRED = b"1" HOMEKIT_STATUS_PAIRED = b"0" -@pytest.fixture -def mock_zeroconf(): - """Mock zeroconf.""" - with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: - yield mock_zc.return_value - - def service_update_mock(zeroconf, services, handlers): """Call service update handler.""" for service in services: diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py new file mode 100644 index 00000000000..0a2095daa6a --- /dev/null +++ b/tests/components/zeroconf/test_usage.py @@ -0,0 +1,56 @@ +"""Test Zeroconf multiple instance protection.""" +import zeroconf + +from homeassistant.components.zeroconf import async_get_instance +from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_catcher + +from tests.async_mock import Mock, patch + + +async def test_multiple_zeroconf_instances(hass, mock_zeroconf, caplog): + """Test creating multiple zeroconf throws without an integration.""" + + zeroconf_instance = await async_get_instance(hass) + + install_multiple_zeroconf_catcher(zeroconf_instance) + + new_zeroconf_instance = zeroconf.Zeroconf() + assert new_zeroconf_instance == zeroconf_instance + + assert "Zeroconf" in caplog.text + + +async def test_multiple_zeroconf_instances_gives_shared(hass, mock_zeroconf, caplog): + """Test creating multiple zeroconf gives the shared instance to an integration.""" + + zeroconf_instance = await async_get_instance(hass) + + install_multiple_zeroconf_catcher(zeroconf_instance) + + correct_frame = Mock( + filename="/config/custom_components/burncpu/light.py", + lineno="23", + line="self.light.is_on", + ) + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/dev/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock( + filename="/home/dev/homeassistant/components/zeroconf/usage.py", + lineno="23", + line="self.light.is_on", + ), + Mock(filename="/home/dev/mdns/lights.py", lineno="2", line="something()",), + ], + ): + assert zeroconf.Zeroconf() == zeroconf_instance + + assert "custom_components/burncpu/light.py" in caplog.text + assert "23" in caplog.text + assert "self.light.is_on" in caplog.text diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 2e8c83c6517..9f68ecdefb2 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -36,6 +36,39 @@ async def test_extract_frame_integration(caplog): assert found_frame == correct_frame +async def test_extract_frame_integration_with_excluded_intergration(caplog): + """Test extracting the current frame from integration context.""" + correct_frame = Mock( + filename="/home/dev/homeassistant/components/mdns/light.py", + lineno="23", + line="self.light.is_on", + ) + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/dev/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock( + filename="/home/dev/homeassistant/components/zeroconf/usage.py", + lineno="23", + line="self.light.is_on", + ), + Mock(filename="/home/dev/mdns/lights.py", lineno="2", line="something()",), + ], + ): + found_frame, integration, path = frame.get_integration_frame( + exclude_integrations={"zeroconf"} + ) + + assert integration == "mdns" + assert path == "homeassistant/components/" + assert found_frame == correct_frame + + async def test_extract_frame_no_integration(caplog): """Test extracting the current frame without integration context.""" with patch( From fbf44b37a98337e15eb6ec16ab65fb24b6eb41f5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 12 Aug 2020 10:50:36 -0400 Subject: [PATCH 129/862] Simplify vizio unique ID check since only IP and device class are needed (#37692) --- homeassistant/components/vizio/config_flow.py | 89 +++++------------ homeassistant/components/vizio/manifest.json | 2 +- .../components/vizio/media_player.py | 10 +- homeassistant/components/vizio/strings.json | 5 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vizio/conftest.py | 23 ++++- tests/components/vizio/test_config_flow.py | 95 +++++++------------ 8 files changed, 88 insertions(+), 140 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 335f138ee7e..fcb41a5891b 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -180,14 +180,8 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data = None self._apps = {} - async def _create_entry_if_unique( - self, input_dict: Dict[str, Any] - ) -> Dict[str, Any]: - """ - Create entry if ID is unique. - - If it is, create entry. If it isn't, abort config flow. - """ + async def _create_entry(self, input_dict: Dict[str, Any]) -> Dict[str, Any]: + """Create vizio config entry.""" # Remove extra keys that will not be used by entry setup input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None) input_dict.pop(CONF_INCLUDE_OR_EXCLUDE, None) @@ -206,19 +200,25 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: # Store current values in case setup fails and user needs to edit self._user_schema = _get_config_schema(user_input) + unique_id = await VizioAsync.get_unique_id( + user_input[CONF_HOST], + user_input[CONF_DEVICE_CLASS], + session=async_get_clientsession(self.hass, False), + ) - # Check if new config entry matches any existing config entries - for entry in self.hass.config_entries.async_entries(DOMAIN): - # If source is ignore bypass host and name check and continue through loop - if entry.source == SOURCE_IGNORE: - continue - if await self.hass.async_add_executor_job( - _host_is_same, entry.data[CONF_HOST], user_input[CONF_HOST] - ): - errors[CONF_HOST] = "host_exists" - - if entry.data[CONF_NAME] == user_input[CONF_NAME]: - errors[CONF_NAME] = "name_exists" + if not unique_id: + errors[CONF_HOST] = "cannot_connect" + else: + # Set unique ID and abort if a flow with the same unique ID is already in progress + existing_entry = await self.async_set_unique_id( + unique_id=unique_id, raise_on_progress=True + ) + # If device was discovered, abort if existing entry found, otherwise display an error + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + if self.context["source"] == SOURCE_ZEROCONF: + self._abort_if_unique_id_configured() + elif existing_entry: + errors[CONF_HOST] = "existing_config_entry_found" if not errors: # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 @@ -239,21 +239,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" if not errors: - unique_id = await VizioAsync.get_unique_id( - user_input[CONF_HOST], - user_input.get(CONF_ACCESS_TOKEN), - user_input[CONF_DEVICE_CLASS], - session=async_get_clientsession(self.hass, False), - ) - - # Set unique ID and abort if unique ID is already configured on an entry or a flow - # with the unique ID is already in progress - await self.async_set_unique_id( - unique_id=unique_id, raise_on_progress=True - ) - self._abort_if_unique_id_configured() - - return await self._create_entry_if_unique(user_input) + return await self._create_entry(user_input) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 elif self._must_show_form and self.context["source"] == SOURCE_IMPORT: # Import should always display the config form if CONF_ACCESS_TOKEN @@ -350,27 +336,10 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> Dict[str, Any]: """Handle zeroconf discovery.""" - # Set unique ID early to prevent device from getting rediscovered multiple times - await self.async_set_unique_id( - unique_id=discovery_info[CONF_HOST].split(":")[0], raise_on_progress=True - ) - self._abort_if_unique_id_configured() - discovery_info[ CONF_HOST ] = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" - # Check if new config entry matches any existing config entries and abort if so - for entry in self.hass.config_entries.async_entries(DOMAIN): - # If source is ignore bypass host check and continue through loop - if entry.source == SOURCE_IGNORE: - continue - - if await self.hass.async_add_executor_job( - _host_is_same, entry.data[CONF_HOST], discovery_info[CONF_HOST] - ): - return self.async_abort(reason="already_configured_device") - # Set default name to discovered device name by stripping zeroconf service # (`type`) from `name` num_chars_to_strip = len(discovery_info[CONF_TYPE]) + 1 @@ -436,20 +405,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data[CONF_ACCESS_TOKEN] = pair_data.auth_token self._must_show_form = True - unique_id = await VizioAsync.get_unique_id( - self._data[CONF_HOST], - self._data[CONF_ACCESS_TOKEN], - self._data[CONF_DEVICE_CLASS], - session=async_get_clientsession(self.hass, False), - ) - - # Set unique ID and abort if unique ID is already configured on an entry or a flow - # with the unique ID is already in progress - await self.async_set_unique_id( - unique_id=unique_id, raise_on_progress=True - ) - self._abort_if_unique_id_configured() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 if self.context["source"] == SOURCE_IMPORT: # If user is pairing via config import, show different message @@ -470,7 +425,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _pairing_complete(self, step_id: str) -> Dict[str, Any]: """Handle config flow completion.""" if not self._must_show_form: - return await self._create_entry_if_unique(self._data) + return await self._create_entry(self._data) self._must_show_form = False return self.async_show_form( diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 6223a95821c..a0c36dc0089 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "VIZIO SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.49"], + "requirements": ["pyvizio==0.1.51"], "codeowners": ["@raman325"], "config_flow": true, "zeroconf": ["_viziocast._tcp.local."], diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 408e37f011b..a2f67b1d5c8 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -294,7 +294,7 @@ class VizioDevice(MediaPlayerEntity): setting_type, setting_name, new_value, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks when entity is added.""" # Register callback for when config entry is updated. self._async_unsub_listeners.append( @@ -310,7 +310,7 @@ class VizioDevice(MediaPlayerEntity): ) ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks when entity is removed.""" for listener in self._async_unsub_listeners: listener() @@ -323,7 +323,7 @@ class VizioDevice(MediaPlayerEntity): return self._available @property - def state(self) -> str: + def state(self) -> Optional[str]: """Return the state of the device.""" return self._state @@ -338,7 +338,7 @@ class VizioDevice(MediaPlayerEntity): return self._icon @property - def volume_level(self) -> float: + def volume_level(self) -> Optional[float]: """Return the volume level of the device.""" return self._volume_level @@ -348,7 +348,7 @@ class VizioDevice(MediaPlayerEntity): return self._is_volume_muted @property - def source(self) -> str: + def source(self) -> Optional[str]: """Return current input of the device.""" if self._current_app is not None and self._current_input in INPUT_APPS: return self._current_app diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 0de1a380d12..8979f6fd82e 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -28,10 +28,9 @@ } }, "error": { - "host_exists": "[%key:component::vizio::config::step::user::title%] with specified host already configured.", - "name_exists": "[%key:component::vizio::config::step::user::title%] with specified name already configured.", "complete_pairing_failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "existing_config_entry_found": "An existing [%key:component::vizio::config::step::user::title%] config entry with the same serial number has already been configured. You must delete the existing entry in order to configure this one." }, "abort": { "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/requirements_all.txt b/requirements_all.txt index 810fd65fab5..93cf481ec4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1834,7 +1834,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.49 +pyvizio==0.1.51 # homeassistant.components.velux pyvlx==0.2.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee2f251428e..ed62aeee367 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -839,7 +839,7 @@ pyvera==0.3.9 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.49 +pyvizio==0.1.51 # homeassistant.components.volumio pyvolumio==0.1.1 diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index fd5ff7cd468..5daedf6fa57 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -47,15 +47,32 @@ def skip_notifications_fixture(): yield +@pytest.fixture(name="vizio_get_unique_id", autouse=True) +def vizio_get_unique_id_fixture(): + """Mock get vizio unique ID.""" + with patch( + "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", + return_value=UNIQUE_ID, + ): + yield + + +@pytest.fixture(name="vizio_no_unique_id") +def vizio_no_unique_id_fixture(): + """Mock no vizio unique ID returrned.""" + with patch( + "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", + return_value=None, + ): + yield + + @pytest.fixture(name="vizio_connect") def vizio_connect_fixture(): """Mock valid vizio device and entry setup.""" with patch( "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", return_value=True, - ), patch( - "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", - return_value=UNIQUE_ID, ): yield diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 74d4d6a2c62..41490e9f4be 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -51,7 +51,6 @@ from .const import ( NAME2, UNIQUE_ID, VOLUME_STEP, - ZEROCONF_HOST, ) from tests.common import MockConfigEntry @@ -223,7 +222,10 @@ async def test_user_host_already_configured( ) -> None: """Test host is already configured during user setup.""" entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP} + domain=DOMAIN, + data=MOCK_SPEAKER_CONFIG, + options={CONF_VOLUME_STEP: VOLUME_STEP}, + unique_id=UNIQUE_ID, ) entry.add_to_hass(hass) fail_entry = MOCK_SPEAKER_CONFIG.copy() @@ -234,61 +236,15 @@ async def test_user_host_already_configured( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_HOST: "host_exists"} + assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} -async def test_user_host_already_configured_no_port( +async def test_user_serial_number_already_exists( hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, ) -> None: - """Test host is already configured during user setup when existing entry has no port.""" - # Mock entry without port so we can test that the same entry WITH a port will fail - no_port_entry = MOCK_SPEAKER_CONFIG.copy() - no_port_entry[CONF_HOST] = no_port_entry[CONF_HOST].split(":")[0] - entry = MockConfigEntry( - domain=DOMAIN, data=no_port_entry, options={CONF_VOLUME_STEP: VOLUME_STEP} - ) - entry.add_to_hass(hass) - fail_entry = MOCK_SPEAKER_CONFIG.copy() - fail_entry[CONF_NAME] = "newtestname" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=fail_entry - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_HOST: "host_exists"} - - -async def test_user_name_already_configured( - hass: HomeAssistantType, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: - """Test name is already configured during user setup.""" - entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP} - ) - entry.add_to_hass(hass) - - fail_entry = MOCK_SPEAKER_CONFIG.copy() - fail_entry[CONF_HOST] = "0.0.0.0" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=fail_entry - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_NAME: "name_exists"} - - -async def test_user_esn_already_exists( - hass: HomeAssistantType, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: - """Test ESN is already configured with different host and name during user setup.""" + """Test serial_number is already configured with different host and name during user setup.""" # Set up new entry MockConfigEntry( domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID @@ -303,14 +259,26 @@ async def test_user_esn_already_exists( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} async def test_user_error_on_could_not_connect( + hass: HomeAssistantType, vizio_no_unique_id: pytest.fixture +) -> None: + """Test with could_not_connect during user setup due to no connectivity.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "cannot_connect"} + + +async def test_user_error_on_could_not_connect_invalid_token( hass: HomeAssistantType, vizio_cant_connect: pytest.fixture ) -> None: - """Test with could_not_connect during user_setup.""" + """Test with could_not_connect during user setup due to invalid token.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) @@ -683,6 +651,7 @@ async def test_import_error( domain=DOMAIN, data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG), options={CONF_VOLUME_STEP: VOLUME_STEP}, + unique_id=UNIQUE_ID, ) entry.add_to_hass(hass) fail_entry = MOCK_SPEAKER_CONFIG.copy() @@ -763,10 +732,14 @@ async def test_zeroconf_flow_already_configured( hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, + vizio_guess_device_type: pytest.fixture, ) -> None: """Test entity is already configured during zeroconf setup.""" entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP} + domain=DOMAIN, + data=MOCK_SPEAKER_CONFIG, + options={CONF_VOLUME_STEP: VOLUME_STEP}, + unique_id=UNIQUE_ID, ) entry.add_to_hass(hass) @@ -778,7 +751,7 @@ async def test_zeroconf_flow_already_configured( # Flow should abort because device is already setup assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured_device" + assert result["reason"] == "already_configured" async def test_zeroconf_dupe_fail( @@ -842,7 +815,7 @@ async def test_zeroconf_abort_when_ignored( data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP}, source=SOURCE_IGNORE, - unique_id=ZEROCONF_HOST, + unique_id=UNIQUE_ID, ) entry.add_to_hass(hass) @@ -860,12 +833,16 @@ async def test_zeroconf_flow_already_configured_hostname( vizio_connect: pytest.fixture, vizio_bypass_setup: pytest.fixture, vizio_hostname_check: pytest.fixture, + vizio_guess_device_type: pytest.fixture, ) -> None: """Test entity is already configured during zeroconf setup when existing entry uses hostname.""" config = MOCK_SPEAKER_CONFIG.copy() config[CONF_HOST] = "hostname" entry = MockConfigEntry( - domain=DOMAIN, data=config, options={CONF_VOLUME_STEP: VOLUME_STEP} + domain=DOMAIN, + data=config, + options={CONF_VOLUME_STEP: VOLUME_STEP}, + unique_id=UNIQUE_ID, ) entry.add_to_hass(hass) @@ -877,7 +854,7 @@ async def test_zeroconf_flow_already_configured_hostname( # Flow should abort because device is already setup assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured_device" + assert result["reason"] == "already_configured" async def test_import_flow_already_configured_hostname( From 716fa63e73f7ab8a320aa50584b30ce6626fd851 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 12 Aug 2020 11:39:05 -0500 Subject: [PATCH 130/862] Update script helper constructor parameters (#38763) Add domain and make it and name required. Add optional running_description. --- .../components/automation/__init__.py | 2 + .../components/intent_script/__init__.py | 2 +- homeassistant/components/knx/__init__.py | 2 +- homeassistant/components/kodi/media_player.py | 4 +- .../components/lg_netcast/media_player.py | 3 +- .../components/panasonic_viera/__init__.py | 2 +- .../components/philips_js/media_player.py | 3 +- .../components/samsungtv/media_player.py | 13 +- homeassistant/components/script/__init__.py | 10 +- .../template/alarm_control_panel.py | 9 +- homeassistant/components/template/cover.py | 11 +- homeassistant/components/template/fan.py | 18 ++- homeassistant/components/template/light.py | 17 ++- homeassistant/components/template/lock.py | 5 +- homeassistant/components/template/switch.py | 5 +- homeassistant/components/template/vacuum.py | 22 ++- .../components/wake_on_lan/switch.py | 5 +- .../components/webostv/media_player.py | 2 +- homeassistant/helpers/script.py | 26 ++-- tests/helpers/test_script.py | 133 ++++++++++++------ 20 files changed, 194 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f7a6aecf03b..5da1aedab25 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -545,6 +545,8 @@ async def _async_process_config(hass, config, component): hass, config_block[CONF_ACTION], name, + DOMAIN, + running_description="automation actions", script_mode=config_block[CONF_MODE], max_runs=config_block[CONF_MAX], logger=_LOGGER, diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 38f93ed3506..d69a78228e7 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -55,7 +55,7 @@ async def async_setup(hass, config): for intent_type, conf in intents.items(): if CONF_ACTION in conf: conf[CONF_ACTION] = script.Script( - hass, conf[CONF_ACTION], f"Intent Script {intent_type}" + hass, conf[CONF_ACTION], f"Intent Script {intent_type}", DOMAIN ) intent.async_register(hass, ScriptIntentHandler(intent_type, conf)) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1ef50899cb5..d5bfdcc0e57 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -310,7 +310,7 @@ class KNXAutomation: self.hass = hass self.device = device script_name = f"{device.get_name()} turn ON script" - self.script = Script(hass, action, script_name) + self.script = Script(hass, action, script_name, DOMAIN) self.action = ActionCallback( hass.data[DATA_KNX].xknx, self.script.async_run, hook=hook, counter=counter diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index e2dc15bab41..85fe152b21a 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -327,13 +327,15 @@ class KodiDevice(MediaPlayerEntity): self.hass, turn_on_action, f"{self.name} turn ON script", - self.async_update_ha_state(True), + DOMAIN, + change_listener=self.async_update_ha_state(True), ) if turn_off_action is not None: turn_off_action = script.Script( self.hass, _check_deprecated_turn_off(hass, turn_off_action), f"{self.name} turn OFF script", + DOMAIN, ) self._turn_on_action = turn_on_action self._turn_off_action = turn_off_action diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index d4e5fc119ac..5a2f289ff32 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -70,7 +70,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): on_action = config.get(CONF_ON_ACTION) client = LgNetCastClient(host, access_token) - on_action_script = Script(hass, on_action) if on_action else None + domain = __name__.split(".")[-2] + on_action_script = Script(hass, on_action, name, domain) if on_action else None add_entities([LgTVDevice(client, name, on_action_script)], True) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index ebc1c20a1fa..8d7c35ccc9c 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass, config_entry): on_action = config[CONF_ON_ACTION] if on_action is not None: - on_action = Script(hass, on_action) + on_action = Script(hass, on_action, config[CONF_NAME], DOMAIN) params = {} if CONF_APP_ID in config and CONF_ENCRYPTION_KEY in config: diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index f72aef2c464..d477c7751cb 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -77,7 +77,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): turn_on_action = config.get(CONF_ON_ACTION) tvapi = PhilipsTV(host, api_version) - on_script = Script(hass, turn_on_action) if turn_on_action else None + domain = __name__.split(".")[-2] + on_script = Script(hass, turn_on_action, name, domain) if turn_on_action else None add_entities([PhilipsTVMediaPlayer(tvapi, name, on_script)]) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 774027776c4..8c4cd6cfde9 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -34,7 +34,14 @@ from homeassistant.helpers.script import Script from homeassistant.util import dt as dt_util from .bridge import SamsungTVBridge -from .const import CONF_MANUFACTURER, CONF_MODEL, CONF_ON_ACTION, DOMAIN, LOGGER +from .const import ( + CONF_MANUFACTURER, + CONF_MODEL, + CONF_ON_ACTION, + DEFAULT_NAME, + DOMAIN, + LOGGER, +) KEY_PRESS_TIMEOUT = 1.2 SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} @@ -63,7 +70,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): and hass.data[DOMAIN][ip_address][CONF_ON_ACTION] ): turn_on_action = hass.data[DOMAIN][ip_address][CONF_ON_ACTION] - on_script = Script(hass, turn_on_action) + on_script = Script( + hass, turn_on_action, config_entry.data.get(CONF_NAME, DEFAULT_NAME), DOMAIN + ) # Initialize bridge data = config_entry.data.copy() diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index e12e2abd312..b6d02872adb 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -255,10 +255,12 @@ class ScriptEntity(ToggleEntity): hass, cfg[CONF_SEQUENCE], cfg.get(CONF_ALIAS, object_id), - self.async_change_listener, - cfg[CONF_MODE], - cfg[CONF_MAX], - logging.getLogger(f"{__name__}.{object_id}"), + DOMAIN, + running_description="script sequence", + change_listener=self.async_change_listener, + script_mode=cfg[CONF_MODE], + max_runs=cfg[CONF_MAX], + logger=logging.getLogger(f"{__name__}.{object_id}"), ) self._changed = asyncio.Event() diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 4209388ae8a..36cdcf3a55e 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -147,17 +147,18 @@ class AlarmControlPanelTemplate(AlarmControlPanelEntity): self._template = state_template self._disarm_script = None self._code_arm_required = code_arm_required + domain = __name__.split(".")[-2] if disarm_action is not None: - self._disarm_script = Script(hass, disarm_action) + self._disarm_script = Script(hass, disarm_action, name, domain) self._arm_away_script = None if arm_away_action is not None: - self._arm_away_script = Script(hass, arm_away_action) + self._arm_away_script = Script(hass, arm_away_action, name, domain) self._arm_home_script = None if arm_home_action is not None: - self._arm_home_script = Script(hass, arm_home_action) + self._arm_home_script = Script(hass, arm_home_action, name, domain) self._arm_night_script = None if arm_night_action is not None: - self._arm_night_script = Script(hass, arm_night_action) + self._arm_night_script = Script(hass, arm_night_action, name, domain) self._state = None self._entities = template_entity_ids diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 08dd18ae3a4..3f92949b4ad 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -205,20 +205,21 @@ class CoverTemplate(CoverEntity): self._entity_picture_template = entity_picture_template self._availability_template = availability_template self._open_script = None + domain = __name__.split(".")[-2] if open_action is not None: - self._open_script = Script(hass, open_action) + self._open_script = Script(hass, open_action, friendly_name, domain) self._close_script = None if close_action is not None: - self._close_script = Script(hass, close_action) + self._close_script = Script(hass, close_action, friendly_name, domain) self._stop_script = None if stop_action is not None: - self._stop_script = Script(hass, stop_action) + self._stop_script = Script(hass, stop_action, friendly_name, domain) self._position_script = None if position_action is not None: - self._position_script = Script(hass, position_action) + self._position_script = Script(hass, position_action, friendly_name, domain) self._tilt_script = None if tilt_action is not None: - self._tilt_script = Script(hass, tilt_action) + self._tilt_script = Script(hass, tilt_action, friendly_name, domain) self._optimistic = optimistic or (not state_template and not position_template) self._tilt_optimistic = tilt_optimistic or not tilt_template self._icon = None diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index a6a0f6b8135..d68f37123bf 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -176,20 +176,28 @@ class TemplateFan(FanEntity): self._available = True self._supported_features = 0 - self._on_script = Script(hass, on_action) - self._off_script = Script(hass, off_action) + domain = __name__.split(".")[-2] + + self._on_script = Script(hass, on_action, friendly_name, domain) + self._off_script = Script(hass, off_action, friendly_name, domain) self._set_speed_script = None if set_speed_action: - self._set_speed_script = Script(hass, set_speed_action) + self._set_speed_script = Script( + hass, set_speed_action, friendly_name, domain + ) self._set_oscillating_script = None if set_oscillating_action: - self._set_oscillating_script = Script(hass, set_oscillating_action) + self._set_oscillating_script = Script( + hass, set_oscillating_action, friendly_name, domain + ) self._set_direction_script = None if set_direction_action: - self._set_direction_script = Script(hass, set_direction_action) + self._set_direction_script = Script( + hass, set_direction_action, friendly_name, domain + ) self._state = STATE_OFF self._speed = None diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index b85aa6f3a95..52fa929d17b 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -186,23 +186,28 @@ class LightTemplate(LightEntity): self._icon_template = icon_template self._entity_picture_template = entity_picture_template self._availability_template = availability_template - self._on_script = Script(hass, on_action) - self._off_script = Script(hass, off_action) + domain = __name__.split(".")[-2] + self._on_script = Script(hass, on_action, friendly_name, domain) + self._off_script = Script(hass, off_action, friendly_name, domain) self._level_script = None if level_action is not None: - self._level_script = Script(hass, level_action) + self._level_script = Script(hass, level_action, friendly_name, domain) self._level_template = level_template self._temperature_script = None if temperature_action is not None: - self._temperature_script = Script(hass, temperature_action) + self._temperature_script = Script( + hass, temperature_action, friendly_name, domain + ) self._temperature_template = temperature_template self._color_script = None if color_action is not None: - self._color_script = Script(hass, color_action) + self._color_script = Script(hass, color_action, friendly_name, domain) self._color_template = color_template self._white_value_script = None if white_value_action is not None: - self._white_value_script = Script(hass, white_value_action) + self._white_value_script = Script( + hass, white_value_action, friendly_name, domain + ) self._white_value_template = white_value_template self._state = False diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 07aeda70be1..427afe6808b 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -97,8 +97,9 @@ class TemplateLock(LockEntity): self._state_template = value_template self._availability_template = availability_template self._state_entities = entity_ids - self._command_lock = Script(hass, command_lock) - self._command_unlock = Script(hass, command_unlock) + domain = __name__.split(".")[-2] + self._command_lock = Script(hass, command_lock, name, domain) + self._command_unlock = Script(hass, command_unlock, name, domain) self._optimistic = optimistic self._available = True self._unique_id = unique_id diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index f9b07fa1dec..5bd5f69cc7b 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -126,8 +126,9 @@ class SwitchTemplate(SwitchEntity, RestoreEntity): ) self._name = friendly_name self._template = state_template - self._on_script = Script(hass, on_action) - self._off_script = Script(hass, off_action) + domain = __name__.split(".")[-2] + self._on_script = Script(hass, on_action, friendly_name, domain) + self._off_script = Script(hass, off_action, friendly_name, domain) self._state = False self._icon_template = icon_template self._entity_picture_template = entity_picture_template diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index a61a1690e5a..24002b2b793 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -195,36 +195,44 @@ class TemplateVacuum(StateVacuumEntity): self._attribute_templates = attribute_templates self._attributes = {} - self._start_script = Script(hass, start_action) + domain = __name__.split(".")[-2] + + self._start_script = Script(hass, start_action, friendly_name, domain) self._pause_script = None if pause_action: - self._pause_script = Script(hass, pause_action) + self._pause_script = Script(hass, pause_action, friendly_name, domain) self._supported_features |= SUPPORT_PAUSE self._stop_script = None if stop_action: - self._stop_script = Script(hass, stop_action) + self._stop_script = Script(hass, stop_action, friendly_name, domain) self._supported_features |= SUPPORT_STOP self._return_to_base_script = None if return_to_base_action: - self._return_to_base_script = Script(hass, return_to_base_action) + self._return_to_base_script = Script( + hass, return_to_base_action, friendly_name, domain + ) self._supported_features |= SUPPORT_RETURN_HOME self._clean_spot_script = None if clean_spot_action: - self._clean_spot_script = Script(hass, clean_spot_action) + self._clean_spot_script = Script( + hass, clean_spot_action, friendly_name, domain + ) self._supported_features |= SUPPORT_CLEAN_SPOT self._locate_script = None if locate_action: - self._locate_script = Script(hass, locate_action) + self._locate_script = Script(hass, locate_action, friendly_name, domain) self._supported_features |= SUPPORT_LOCATE self._set_fan_speed_script = None if set_fan_speed_action: - self._set_fan_speed_script = Script(hass, set_fan_speed_action) + self._set_fan_speed_script = Script( + hass, set_fan_speed_action, friendly_name, domain + ) self._supported_features |= SUPPORT_FAN_SPEED self._state = None diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index dc6ea358acd..94bc83b7dd4 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -81,7 +81,10 @@ class WolSwitch(SwitchEntity): self._mac_address = mac_address self._broadcast_address = broadcast_address self._broadcast_port = broadcast_port - self._off_script = Script(hass, off_action) if off_action else None + domain = __name__.split(".")[-2] + self._off_script = ( + Script(hass, off_action, name, domain) if off_action else None + ) self._state = False @property diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 1cbf844b289..d44b5fb6eaf 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -75,7 +75,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= turn_on_action = discovery_info.get(CONF_ON_ACTION) client = hass.data[DOMAIN][host]["client"] - on_script = Script(hass, turn_on_action) if turn_on_action else None + on_script = Script(hass, turn_on_action, name, DOMAIN) if turn_on_action else None entity = LgWebOSMediaPlayerEntity(client, name, customize, on_script) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 6fed54227a3..9b2d9a9a990 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -176,7 +176,7 @@ class _ScriptRun: try: if self._stop.is_set(): return - self._log("Running script") + self._log("Running %s", self._script.running_description) for self._step, self._action in enumerate(self._script.sequence): if self._stop.is_set(): break @@ -615,7 +615,11 @@ class Script: self, hass: HomeAssistant, sequence: Sequence[Dict[str, Any]], - name: Optional[str] = None, + name: str, + domain: str, + *, + # Used in "Running " log message + running_description: Optional[str] = None, change_listener: Optional[Callable[..., Any]] = None, script_mode: str = DEFAULT_SCRIPT_MODE, max_runs: int = DEFAULT_MAX, @@ -640,6 +644,8 @@ class Script: self.sequence = sequence template.attach(hass, self.sequence) self.name = name + self.domain = domain + self.running_description = running_description or f"{domain} script" self.change_listener = change_listener self.script_mode = script_mode self._set_logger(logger) @@ -662,10 +668,7 @@ class Script: if logger: self._logger = logger else: - logger_name = __name__ - if self.name: - logger_name = ".".join([logger_name, slugify(self.name)]) - self._logger = logging.getLogger(logger_name) + self._logger = logging.getLogger(f"{__name__}.{slugify(self.name)}") def update_logger(self, logger: Optional[logging.Logger] = None) -> None: """Update logger.""" @@ -832,6 +835,8 @@ class Script: self._hass, action[CONF_REPEAT][CONF_SEQUENCE], f"{self.name}: {step_name}", + self.domain, + running_description=self.running_description, script_mode=SCRIPT_MODE_PARALLEL, max_runs=self.max_runs, logger=self._logger, @@ -860,6 +865,8 @@ class Script: self._hass, choice[CONF_SEQUENCE], f"{self.name}: {step_name}: choice {idx}", + self.domain, + running_description=self.running_description, script_mode=SCRIPT_MODE_PARALLEL, max_runs=self.max_runs, logger=self._logger, @@ -875,6 +882,8 @@ class Script: self._hass, action[CONF_DEFAULT], f"{self.name}: {step_name}: default", + self.domain, + running_description=self.running_description, script_mode=SCRIPT_MODE_PARALLEL, max_runs=self.max_runs, logger=self._logger, @@ -896,9 +905,8 @@ class Script: return choose_data def _log(self, msg, *args, level=logging.INFO): - if self.name: - msg = f"%s: {msg}" - args = [self.name, *args] + msg = f"%s: {msg}" + args = [self.name, *args] if level == _LOG_EXCEPTION: self._logger.exception(msg, *args) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index fbfd06aa930..968826dd2d5 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -85,14 +85,16 @@ def async_watch_for_action(script_obj, message): return flag -async def test_firing_event_basic(hass): +async def test_firing_event_basic(hass, caplog): """Test the firing of events.""" event = "test_event" context = Context() events = async_capture_events(hass, event) sequence = cv.SCRIPT_SCHEMA({"event": event, "event_data": {"hello": "world"}}) - script_obj = script.Script(hass, sequence) + script_obj = script.Script( + hass, sequence, "Test Name", "test_domain", running_description="test script" + ) await script_obj.async_run(context=context) await hass.async_block_till_done() @@ -100,6 +102,8 @@ async def test_firing_event_basic(hass): assert len(events) == 1 assert events[0].context is context assert events[0].data.get("hello") == "world" + assert ".test_name:" in caplog.text + assert "Test Name: Running test script" in caplog.text async def test_firing_event_template(hass): @@ -121,7 +125,7 @@ async def test_firing_event_template(hass): }, } ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run(MappingProxyType({"is_world": "yes"}), context=context) await hass.async_block_till_done() @@ -140,7 +144,7 @@ async def test_calling_service_basic(hass): calls = async_mock_service(hass, "test", "script") sequence = cv.SCRIPT_SCHEMA({"service": "test.script", "data": {"hello": "world"}}) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run(context=context) await hass.async_block_till_done() @@ -174,7 +178,7 @@ async def test_calling_service_template(hass): }, } ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run(MappingProxyType({"is_world": "yes"}), context=context) await hass.async_block_till_done() @@ -227,7 +231,9 @@ async def test_multiple_runs_no_wait(hass): }, ] ) - script_obj = script.Script(hass, sequence, script_mode="parallel", max_runs=2) + script_obj = script.Script( + hass, sequence, "Test Name", "test_domain", script_mode="parallel", max_runs=2 + ) # Start script twice in such a way that second run will be started while first run # is in the middle of the first service call. @@ -259,7 +265,7 @@ async def test_activating_scene(hass): calls = async_mock_service(hass, scene.DOMAIN, SERVICE_TURN_ON) sequence = cv.SCRIPT_SCHEMA({"scene": "scene.hello"}) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run(context=context) await hass.async_block_till_done() @@ -285,7 +291,14 @@ async def test_stop_no_wait(hass, count): hass.services.async_register("test", "script", async_simulate_long_service) sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) - script_obj = script.Script(hass, sequence, script_mode="parallel", max_runs=count) + script_obj = script.Script( + hass, + sequence, + "Test Name", + "test_domain", + script_mode="parallel", + max_runs=count, + ) # Get script started specified number of times and wait until the test.script # service has started for each run. @@ -317,7 +330,7 @@ async def test_delay_basic(hass, mock_timeout): """Test the delay.""" delay_alias = "delay step" sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": 5}, "alias": delay_alias}) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") delay_started_flag = async_watch_for_action(script_obj, delay_alias) try: @@ -349,7 +362,9 @@ async def test_multiple_runs_delay(hass, mock_timeout): {"event": event, "event_data": {"value": 2}}, ] ) - script_obj = script.Script(hass, sequence, script_mode="parallel", max_runs=2) + script_obj = script.Script( + hass, sequence, "Test Name", "test_domain", script_mode="parallel", max_runs=2 + ) delay_started_flag = async_watch_for_action(script_obj, "delay") try: @@ -381,7 +396,7 @@ async def test_multiple_runs_delay(hass, mock_timeout): async def test_delay_template_ok(hass, mock_timeout): """Test the delay as a template.""" sequence = cv.SCRIPT_SCHEMA({"delay": "00:00:{{ 5 }}"}) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") delay_started_flag = async_watch_for_action(script_obj, "delay") try: @@ -411,7 +426,7 @@ async def test_delay_template_invalid(hass, caplog): {"event": event}, ] ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") start_idx = len(caplog.records) await script_obj.async_run() @@ -429,7 +444,7 @@ async def test_delay_template_invalid(hass, caplog): async def test_delay_template_complex_ok(hass, mock_timeout): """Test the delay with a working complex template.""" sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": "{{ 5 }}"}}) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") delay_started_flag = async_watch_for_action(script_obj, "delay") try: @@ -458,7 +473,7 @@ async def test_delay_template_complex_invalid(hass, caplog): {"event": event}, ] ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") start_idx = len(caplog.records) await script_obj.async_run() @@ -478,7 +493,7 @@ async def test_cancel_delay(hass): event = "test_event" events = async_capture_events(hass, event) sequence = cv.SCRIPT_SCHEMA([{"delay": {"seconds": 5}}, {"event": event}]) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") delay_started_flag = async_watch_for_action(script_obj, "delay") try: @@ -513,7 +528,7 @@ async def test_wait_template_basic(hass): "alias": wait_alias, } ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, wait_alias) try: @@ -545,7 +560,9 @@ async def test_multiple_runs_wait_template(hass): {"event": event, "event_data": {"value": 2}}, ] ) - script_obj = script.Script(hass, sequence, script_mode="parallel", max_runs=2) + script_obj = script.Script( + hass, sequence, "Test Name", "test_domain", script_mode="parallel", max_runs=2 + ) wait_started_flag = async_watch_for_action(script_obj, "wait") try: @@ -582,7 +599,7 @@ async def test_cancel_wait_template(hass): {"event": event}, ] ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") try: @@ -620,7 +637,7 @@ async def test_wait_template_not_schedule(hass): {"event": event}, ] ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") hass.states.async_set("switch.test", "on") await script_obj.async_run() @@ -644,7 +661,7 @@ async def test_wait_template_timeout(hass, mock_timeout, continue_on_timeout, n_ if continue_on_timeout is not None: sequence[0]["continue_on_timeout"] = continue_on_timeout sequence = cv.SCRIPT_SCHEMA(sequence) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") try: @@ -668,7 +685,7 @@ async def test_wait_template_timeout(hass, mock_timeout, continue_on_timeout, n_ async def test_wait_template_variables(hass): """Test the wait template with variables.""" sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ is_state(data, 'off') }}"}) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") try: @@ -703,7 +720,7 @@ async def test_condition_basic(hass): {"event": event}, ] ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") hass.states.async_set("test.entity", "hello") await script_obj.async_run() @@ -728,7 +745,9 @@ async def test_condition_created_once(async_from_config, hass): "value_template": '{{ states.test.entity.state == "hello" }}', } ) - script_obj = script.Script(hass, sequence, script_mode="parallel", max_runs=2) + script_obj = script.Script( + hass, sequence, "Test Name", "test_domain", script_mode="parallel", max_runs=2 + ) async_from_config.reset_mock() @@ -755,7 +774,7 @@ async def test_condition_all_cached(hass): }, ] ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") hass.states.async_set("test.entity", "hello") await script_obj.async_run() @@ -785,7 +804,7 @@ async def test_repeat_count(hass): } } ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run() await hass.async_block_till_done() @@ -829,7 +848,9 @@ async def test_repeat_conditional(hass, condition): "condition": "template", "value_template": "{{ is_state('sensor.test', 'done') }}", } - script_obj = script.Script(hass, cv.SCRIPT_SCHEMA(sequence)) + script_obj = script.Script( + hass, cv.SCRIPT_SCHEMA(sequence), "Test Name", "test_domain" + ) wait_started = async_watch_for_action(script_obj, "wait") hass.states.async_set("sensor.test", "1") @@ -876,7 +897,9 @@ async def test_repeat_var_in_condition(hass, condition): "condition": "template", "value_template": "{{ repeat.index == 2 }}", } - script_obj = script.Script(hass, cv.SCRIPT_SCHEMA(sequence)) + script_obj = script.Script( + hass, cv.SCRIPT_SCHEMA(sequence), "Test Name", "test_domain" + ) with mock.patch( "homeassistant.helpers.condition._LOGGER.error", @@ -956,7 +979,7 @@ async def test_repeat_nested(hass, variables, first_last, inside_x): }, ] ) - script_obj = script.Script(hass, sequence, "test script") + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") with mock.patch( "homeassistant.helpers.condition._LOGGER.error", @@ -1014,7 +1037,7 @@ async def test_choose(hass, var, result): "default": {"event": event, "event_data": {"choice": "default"}}, } ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run(MappingProxyType({"var": var})) await hass.async_block_till_done() @@ -1035,7 +1058,12 @@ async def test_multiple_runs_repeat_choose(hass, caplog, action): """Test parallel runs with repeat & choose actions & max_runs > default.""" max_runs = script.DEFAULT_MAX + 1 script_obj = script.Script( - hass, cv.SCRIPT_SCHEMA(action), script_mode="parallel", max_runs=max_runs + hass, + cv.SCRIPT_SCHEMA(action), + "Test Name", + "test_domain", + script_mode="parallel", + max_runs=max_runs, ) events = async_capture_events(hass, "abc") @@ -1052,7 +1080,7 @@ async def test_last_triggered(hass): """Test the last_triggered.""" event = "test_event" sequence = cv.SCRIPT_SCHEMA({"event": event}) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") assert script_obj.last_triggered is None @@ -1069,7 +1097,7 @@ async def test_propagate_error_service_not_found(hass): event = "test_event" events = async_capture_events(hass, event) sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") with pytest.raises(exceptions.ServiceNotFound): await script_obj.async_run() @@ -1086,7 +1114,7 @@ async def test_propagate_error_invalid_service_data(hass): sequence = cv.SCRIPT_SCHEMA( [{"service": "test.script", "data": {"text": 1}}, {"event": event}] ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") with pytest.raises(vol.Invalid): await script_obj.async_run() @@ -1109,7 +1137,7 @@ async def test_propagate_error_service_exception(hass): hass.services.async_register("test", "script", record_call) sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") with pytest.raises(ValueError): await script_obj.async_run() @@ -1143,6 +1171,8 @@ async def test_referenced_entities(hass): {"delay": "{{ delay_period }}"}, ] ), + "Test Name", + "test_domain", ) assert script_obj.referenced_entities == { "light.service_not_list", @@ -1168,6 +1198,8 @@ async def test_referenced_devices(hass): }, ] ), + "Test Name", + "test_domain", ) assert script_obj.referenced_devices == {"script-dev-id", "condition-dev-id"} # Test we cache results. @@ -1191,7 +1223,7 @@ async def test_script_mode_single(hass, caplog): {"event": event, "event_data": {"value": 2}}, ] ) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") try: @@ -1239,7 +1271,13 @@ async def test_script_mode_2(hass, caplog, script_mode, messages, last_events): logger = logging.getLogger("TEST") max_runs = 1 if script_mode == "restart" else 2 script_obj = script.Script( - hass, sequence, script_mode=script_mode, max_runs=max_runs, logger=logger + hass, + sequence, + "Test Name", + "test_domain", + script_mode=script_mode, + max_runs=max_runs, + logger=logger, ) wait_started_flag = async_watch_for_action(script_obj, "wait") @@ -1303,7 +1341,13 @@ async def test_script_mode_queued(hass): ) logger = logging.getLogger("TEST") script_obj = script.Script( - hass, sequence, script_mode="queued", max_runs=2, logger=logger + hass, + sequence, + "Test Name", + "test_domain", + script_mode="queued", + max_runs=2, + logger=logger, ) watch_messages = [] @@ -1379,7 +1423,8 @@ async def test_script_mode_queued_cancel(hass): script_obj = script.Script( hass, cv.SCRIPT_SCHEMA({"wait_template": "{{ false }}"}), - "test", + "Test Name", + "test_domain", script_mode="queued", max_runs=2, ) @@ -1417,21 +1462,17 @@ async def test_script_mode_queued_cancel(hass): async def test_script_logging(hass, caplog): """Test script logging.""" - script_obj = script.Script(hass, [], "Script with % Name") + script_obj = script.Script(hass, [], "Script with % Name", "test_domain") script_obj._log("Test message with name %s", 1) assert "Script with % Name: Test message with name 1" in caplog.text - script_obj = script.Script(hass, []) - script_obj._log("Test message without name %s", 2) - assert "Test message without name 2" in caplog.text - async def test_shutdown_at(hass, caplog): """Test stopping scripts at shutdown.""" delay_alias = "delay step" sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": 120}, "alias": delay_alias}) - script_obj = script.Script(hass, sequence, "test script") + script_obj = script.Script(hass, sequence, "test script", "test_domain") delay_started_flag = async_watch_for_action(script_obj, delay_alias) try: @@ -1455,7 +1496,7 @@ async def test_shutdown_after(hass, caplog): """Test stopping scripts at shutdown.""" delay_alias = "delay step" sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": 120}, "alias": delay_alias}) - script_obj = script.Script(hass, sequence, "test script") + script_obj = script.Script(hass, sequence, "test script", "test_domain") delay_started_flag = async_watch_for_action(script_obj, delay_alias) hass.state = CoreState.stopping @@ -1485,7 +1526,7 @@ async def test_shutdown_after(hass, caplog): async def test_update_logger(hass, caplog): """Test updating logger.""" sequence = cv.SCRIPT_SCHEMA({"event": "test_event"}) - script_obj = script.Script(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") await script_obj.async_run() await hass.async_block_till_done() From 45526f4e8a2d9657a489fcc9302e4ab11fb761ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 12 Aug 2020 13:30:40 -0500 Subject: [PATCH 131/862] Add async_track_state_added_domain for tracking when states are added to a domain (#38776) * Fire event_state_added when a state is added after start * async_track_state_added_domain * test * naming * coverage --- homeassistant/helpers/event.py | 91 +++++++++++++++++++++++++++++----- tests/helpers/test_event.py | 83 +++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 3f0c2db3b2f..f6c423a35af 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -17,7 +17,14 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + State, + callback, + split_entity_id, +) from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.sun import get_astral_event_next from homeassistant.helpers.template import Template @@ -28,6 +35,9 @@ from homeassistant.util.async_ import run_callback_threadsafe TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks" TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener" +TRACK_STATE_ADDED_DOMAIN_CALLBACKS = "track_state_added_domain_callbacks" +TRACK_STATE_ADDED_DOMAIN_LISTENER = "track_state_added_domain_listener" + TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS = "track_entity_registry_updated_callbacks" TRACK_ENTITY_REGISTRY_UPDATED_LISTENER = "track_entity_registry_updated_listener" @@ -191,7 +201,7 @@ def async_track_state_change_event( @callback def remove_listener() -> None: """Remove state change listener.""" - _async_remove_entity_listeners( + _async_remove_indexed_listeners( hass, TRACK_STATE_CHANGE_CALLBACKS, TRACK_STATE_CHANGE_LISTENER, @@ -203,23 +213,23 @@ def async_track_state_change_event( @callback -def _async_remove_entity_listeners( +def _async_remove_indexed_listeners( hass: HomeAssistant, - storage_key: str, + data_key: str, listener_key: str, - entity_ids: Iterable[str], + storage_keys: Iterable[str], action: Callable[[Event], Any], ) -> None: """Remove a listener.""" - entity_callbacks = hass.data[storage_key] + callbacks = hass.data[data_key] - for entity_id in entity_ids: - entity_callbacks[entity_id].remove(action) - if len(entity_callbacks[entity_id]) == 0: - del entity_callbacks[entity_id] + for storage_key in storage_keys: + callbacks[storage_key].remove(action) + if len(callbacks[storage_key]) == 0: + del callbacks[storage_key] - if not entity_callbacks: + if not callbacks: hass.data[listener_key]() del hass.data[listener_key] @@ -271,7 +281,7 @@ def async_track_entity_registry_updated_event( @callback def remove_listener() -> None: """Remove state change listener.""" - _async_remove_entity_listeners( + _async_remove_indexed_listeners( hass, TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, TRACK_ENTITY_REGISTRY_UPDATED_LISTENER, @@ -282,6 +292,63 @@ def async_track_entity_registry_updated_event( return remove_listener +@bind_hass +def async_track_state_added_domain( + hass: HomeAssistant, + domains: Union[str, Iterable[str]], + action: Callable[[Event], Any], +) -> Callable[[], None]: + """Track state change events when an entity is added to domains.""" + + domain_callbacks = hass.data.setdefault(TRACK_STATE_ADDED_DOMAIN_CALLBACKS, {}) + + if TRACK_STATE_ADDED_DOMAIN_LISTENER not in hass.data: + + @callback + def _async_state_change_dispatcher(event: Event) -> None: + """Dispatch state changes by entity_id.""" + if event.data.get("old_state") is not None: + return + + domain = split_entity_id(event.data["entity_id"])[0] + + if domain not in domain_callbacks: + return + + for action in domain_callbacks[domain][:]: + try: + hass.async_run_job(action, event) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error while processing state added for %s", domain + ) + + hass.data[TRACK_STATE_ADDED_DOMAIN_LISTENER] = hass.bus.async_listen( + EVENT_STATE_CHANGED, _async_state_change_dispatcher + ) + + if isinstance(domains, str): + domains = [domains] + + domains = [domains.lower() for domains in domains] + + for domain in domains: + domain_callbacks.setdefault(domain, []).append(action) + + @callback + def remove_listener() -> None: + """Remove state change listener.""" + _async_remove_indexed_listeners( + hass, + TRACK_STATE_ADDED_DOMAIN_CALLBACKS, + TRACK_STATE_ADDED_DOMAIN_LISTENER, + domains, + action, + ) + + return remove_listener + + @callback @bind_hass def async_track_template( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index aa0a69d1d67..e30f85c9c38 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -16,6 +16,7 @@ from homeassistant.helpers.event import ( async_track_point_in_time, async_track_point_in_utc_time, async_track_same_state, + async_track_state_added_domain, async_track_state_change, async_track_state_change_event, async_track_sunrise, @@ -341,6 +342,88 @@ async def test_async_track_state_change_event(hass): unsub_throws() +async def test_async_track_state_added_domain(hass): + """Test async_track_state_added_domain.""" + 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)) + + @ha.callback + def callback_that_throws(event): + raise ValueError + + unsub_single = async_track_state_added_domain(hass, "light", single_run_callback) + unsub_multi = async_track_state_added_domain( + hass, ["light", "switch"], multiple_run_callback + ) + unsub_throws = async_track_state_added_domain( + hass, ["light", "switch"], callback_that_throws + ) + + # 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 - nothing added so no trigger + hass.states.async_set("light.Bowl", "off") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert len(multiple_entity_id_tracker) == 1 + + # State change off -> off - nothing added so no trigger + hass.states.async_set("light.Bowl", "off", {"some_attr": 1}) + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert len(multiple_entity_id_tracker) == 1 + + # Removing state does not trigger + hass.states.async_remove("light.bowl") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert len(multiple_entity_id_tracker) == 1 + + # 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) == 1 + assert len(multiple_entity_id_tracker) == 2 + + unsub_single() + # Ensure unsubing the listener works + hass.states.async_set("light.new", "off") + await hass.async_block_till_done() + assert len(single_entity_id_tracker) == 1 + assert len(multiple_entity_id_tracker) == 3 + + unsub_multi() + unsub_throws() + + async def test_track_template(hass): """Test tracking template.""" specific_runs = [] From 580e229cf2232e9c00137845647c9ab268318920 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 12 Aug 2020 13:42:06 -0500 Subject: [PATCH 132/862] Create variable with result of wait_template and accept template for timeout option (#38634) --- .../components/automation/numeric_state.py | 23 +-- homeassistant/components/automation/state.py | 21 +-- .../components/automation/template.py | 19 +-- .../components/generic_thermostat/climate.py | 4 +- .../components/ness_alarm/__init__.py | 2 +- .../components/speedtestdotnet/__init__.py | 2 +- .../components/template/binary_sensor.py | 4 +- homeassistant/components/toon/__init__.py | 2 +- homeassistant/components/xiaomi_miio/light.py | 2 +- homeassistant/helpers/config_validation.py | 14 +- homeassistant/helpers/script.py | 42 +++-- tests/helpers/test_script.py | 157 ++++++++++++------ 12 files changed, 164 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 5d3ba863f1f..1429fa7ce7b 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -47,11 +47,7 @@ TRIGGER_SCHEMA = vol.All( vol.Optional(CONF_BELOW): vol.Coerce(float), vol.Optional(CONF_ABOVE): vol.Coerce(float), vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_FOR): vol.Any( - vol.All(cv.time_period, cv.positive_timedelta), - cv.template, - cv.template_complex, - ), + vol.Optional(CONF_FOR): cv.positive_time_period_template, } ), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), @@ -141,20 +137,9 @@ async def async_attach_trigger( } try: - if isinstance(time_delta, template.Template): - period[entity] = vol.All(cv.time_period, cv.positive_timedelta)( - time_delta.async_render(variables) - ) - elif isinstance(time_delta, dict): - time_delta_data = {} - time_delta_data.update( - template.render_complex(time_delta, variables) - ) - period[entity] = vol.All(cv.time_period, cv.positive_timedelta)( - time_delta_data - ) - else: - period[entity] = time_delta + period[entity] = cv.positive_time_period( + template.render_complex(time_delta, variables) + ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 9d504d40de5..603fff5993e 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -33,11 +33,7 @@ TRIGGER_SCHEMA = vol.All( # These are str on purpose. Want to catch YAML conversions vol.Optional(CONF_FROM): vol.Any(str, [str]), vol.Optional(CONF_TO): vol.Any(str, [str]), - vol.Optional(CONF_FOR): vol.Any( - vol.All(cv.time_period, cv.positive_timedelta), - cv.template, - cv.template_complex, - ), + vol.Optional(CONF_FOR): cv.positive_time_period_template, } ), cv.key_dependency(CONF_FOR, CONF_TO), @@ -115,18 +111,9 @@ async def async_attach_trigger( } try: - if isinstance(time_delta, template.Template): - period[entity] = vol.All(cv.time_period, cv.positive_timedelta)( - time_delta.async_render(variables) - ) - elif isinstance(time_delta, dict): - time_delta_data = {} - time_delta_data.update(template.render_complex(time_delta, variables)) - period[entity] = vol.All(cv.time_period, cv.positive_timedelta)( - time_delta_data - ) - else: - period[entity] = time_delta + period[entity] = cv.positive_time_period( + template.render_complex(time_delta, variables) + ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", automation_info["name"], ex diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index ee4484410cd..f376cedd0b0 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -17,11 +17,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "template", vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_FOR): vol.Any( - vol.All(cv.time_period, cv.positive_timedelta), - cv.template, - cv.template_complex, - ), + vol.Optional(CONF_FOR): cv.positive_time_period_template, } ) @@ -73,16 +69,9 @@ async def async_attach_trigger( } try: - if isinstance(time_delta, template.Template): - period = vol.All(cv.time_period, cv.positive_timedelta)( - time_delta.async_render(variables) - ) - elif isinstance(time_delta, dict): - time_delta_data = {} - time_delta_data.update(template.render_complex(time_delta, variables)) - period = vol.All(cv.time_period, cv.positive_timedelta)(time_delta_data) - else: - period = time_delta + period = cv.positive_time_period( + template.render_complex(time_delta, variables) + ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", automation_info["name"], ex diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index be0ec93f225..31231d1ffb2 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -68,13 +68,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_SENSOR): cv.entity_id, vol.Optional(CONF_AC_MODE): cv.boolean, vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), - vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_MIN_DUR): cv.positive_time_period, vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_COLD_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), vol.Optional(CONF_HOT_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), - vol.Optional(CONF_KEEP_ALIVE): vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_KEEP_ALIVE): cv.positive_time_period, vol.Optional(CONF_INITIAL_HVAC_MODE): vol.In( [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF] ), diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index 7131ac505b5..7e5df865aa0 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -56,7 +56,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_DEVICE_PORT): cv.port, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): vol.All(cv.time_period, cv.positive_timedelta), + ): cv.positive_time_period, vol.Optional(CONF_ZONES, default=DEFAULT_ZONES): vol.All( cv.ensure_list, [ZONE_SCHEMA] ), diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 6fd2dec5efd..95b7cdb3d18 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -35,7 +35,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_SERVER_ID): cv.positive_int, vol.Optional( CONF_SCAN_INTERVAL, default=timedelta(minutes=DEFAULT_SCAN_INTERVAL) - ): vol.All(cv.time_period, cv.positive_timedelta), + ): cv.positive_time_period, vol.Optional(CONF_MANUAL, default=False): cv.boolean, vol.Optional( CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 22eb8b9d242..504cf82297f 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -49,8 +49,8 @@ SENSOR_SCHEMA = vol.Schema( vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_DELAY_ON): vol.All(cv.time_period, cv.positive_timedelta), - vol.Optional(CONF_DELAY_OFF): vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_DELAY_ON): cv.positive_time_period, + vol.Optional(CONF_DELAY_OFF): cv.positive_time_period, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index bdfe8e35c74..e2171f02290 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -48,7 +48,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): vol.All(cv.time_period, cv.positive_timedelta), + ): cv.positive_time_period, } ), ) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index cc9343aa2c0..967ea8043f3 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -102,7 +102,7 @@ SERVICE_SCHEMA_SET_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend( ) SERVICE_SCHEMA_SET_DELAYED_TURN_OFF = XIAOMI_MIIO_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_TIME_PERIOD): vol.All(cv.time_period, cv.positive_timedelta)} + {vol.Required(ATTR_TIME_PERIOD): cv.positive_time_period} ) SERVICE_TO_METHOD = { diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e5b113f8a4d..5d883dc8a05 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -402,6 +402,7 @@ def positive_timedelta(value: timedelta) -> timedelta: positive_time_period_dict = vol.All(time_period_dict, positive_timedelta) +positive_time_period = vol.All(time_period, positive_timedelta) def remove_falsy(value: List[T]) -> List[T]: @@ -530,6 +531,11 @@ def template_complex(value: Any) -> Any: return value +positive_time_period_template = vol.Any( + positive_time_period, template, template_complex +) + + def datetime(value: Any) -> datetime_sys: """Validate datetime.""" if isinstance(value, datetime_sys): @@ -876,7 +882,7 @@ STATE_CONDITION_SCHEMA = vol.All( vol.Required(CONF_CONDITION): "state", vol.Required(CONF_ENTITY_ID): entity_ids, vol.Required(CONF_STATE): vol.Any(str, [str]), - vol.Optional(CONF_FOR): vol.All(time_period, positive_timedelta), + vol.Optional(CONF_FOR): positive_time_period, # To support use_trigger_value in automation # Deprecated 2016/04/25 vol.Optional("from"): str, @@ -992,9 +998,7 @@ CONDITION_SCHEMA: vol.Schema = key_value_schemas( _SCRIPT_DELAY_SCHEMA = vol.Schema( { vol.Optional(CONF_ALIAS): string, - vol.Required(CONF_DELAY): vol.Any( - vol.All(time_period, positive_timedelta), template, template_complex - ), + vol.Required(CONF_DELAY): positive_time_period_template, } ) @@ -1002,7 +1006,7 @@ _SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema( { vol.Optional(CONF_ALIAS): string, vol.Required(CONF_WAIT_TEMPLATE): template, - vol.Optional(CONF_TIMEOUT): vol.All(time_period, positive_timedelta), + vol.Optional(CONF_TIMEOUT): positive_time_period_template, vol.Optional(CONF_CONTINUE_ON_TIMEOUT): boolean, } ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 9b2d9a9a990..9f415b10300 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,6 +1,6 @@ """Helpers to execute scripts.""" import asyncio -from datetime import datetime +from datetime import datetime, timedelta from functools import partial import itertools import logging @@ -241,21 +241,25 @@ class _ScriptRun: level=level, ) - async def _async_delay_step(self): - """Handle delay.""" + def _get_pos_time_period_template(self, key): try: - delay = vol.All(cv.time_period, cv.positive_timedelta)( - template.render_complex(self._action[CONF_DELAY], self._variables) + return cv.positive_time_period( + template.render_complex(self._action[key], self._variables) ) except (exceptions.TemplateError, vol.Invalid) as ex: self._log( - "Error rendering %s delay template: %s", + "Error rendering %s %s template: %s", self._script.name, + key, ex, level=logging.ERROR, ) raise _StopScript + async def _async_delay_step(self): + """Handle delay.""" + delay = self._get_pos_time_period_template(CONF_DELAY) + self._script.last_action = self._action.get(CONF_ALIAS, f"delay {delay}") self._log("Executing step %s", self._script.last_action) @@ -269,41 +273,55 @@ class _ScriptRun: async def _async_wait_template_step(self): """Handle a wait template.""" + if CONF_TIMEOUT in self._action: + delay = self._get_pos_time_period_template(CONF_TIMEOUT).total_seconds() + else: + delay = None + self._script.last_action = self._action.get(CONF_ALIAS, "wait template") - self._log("Executing step %s", self._script.last_action) + self._log( + "Executing step %s%s", + self._script.last_action, + "" if delay is None else f" (timeout: {timedelta(seconds=delay)})", + ) + + self._variables["wait"] = {"remaining": delay, "completed": False} wait_template = self._action[CONF_WAIT_TEMPLATE] wait_template.hass = self._hass # check if condition already okay if condition.async_template(self._hass, wait_template, self._variables): + self._variables["wait"]["completed"] = True return @callback def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" + self._variables["wait"] = { + "remaining": to_context.remaining if to_context else delay, + "completed": True, + } done.set() + to_context = None unsub = async_track_template( self._hass, wait_template, async_script_wait, self._variables ) self._changed() - try: - delay = self._action[CONF_TIMEOUT].total_seconds() - except KeyError: - delay = None done = asyncio.Event() tasks = [ self._hass.async_create_task(flag.wait()) for flag in (self._stop, done) ] try: - async with timeout(delay): + async with timeout(delay) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError: if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): self._log(_TIMEOUT_MSG) raise _StopScript + self._variables["wait"]["remaining"] = 0.0 finally: for task in tasks: task.cancel() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 968826dd2d5..305f11b0258 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -16,7 +16,6 @@ import homeassistant.components.scene as scene from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON from homeassistant.core import Context, CoreState, callback from homeassistant.helpers import config_validation as cv, script -from homeassistant.helpers.event import async_call_later import homeassistant.util.dt as dt_util from tests.async_mock import patch @@ -29,49 +28,6 @@ from tests.common import ( ENTITY_ID = "script.test" -@pytest.fixture -def mock_timeout(hass, monkeypatch): - """Mock async_timeout.timeout.""" - - class MockTimeout: - def __init__(self, timeout): - self._timeout = timeout - self._loop = asyncio.get_event_loop() - self._task = None - self._cancelled = False - self._unsub = None - - async def __aenter__(self): - if self._timeout is None: - return self - self._task = asyncio.Task.current_task() - if self._timeout <= 0: - self._loop.call_soon(self._cancel_task) - return self - # Wait for a time_changed event instead of real time passing. - self._unsub = async_call_later(hass, self._timeout, self._cancel_task) - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - if exc_type is asyncio.CancelledError and self._cancelled: - self._unsub = None - self._task = None - raise asyncio.TimeoutError - if self._timeout is not None and self._unsub: - self._unsub() - self._unsub = None - self._task = None - return None - - @callback - def _cancel_task(self, now=None): - if self._task is not None: - self._task.cancel() - self._cancelled = True - - monkeypatch.setattr(script, "timeout", MockTimeout) - - def async_watch_for_action(script_obj, message): """Watch for message in last_action.""" flag = asyncio.Event() @@ -326,7 +282,7 @@ async def test_stop_no_wait(hass, count): assert len(events) == 0 -async def test_delay_basic(hass, mock_timeout): +async def test_delay_basic(hass): """Test the delay.""" delay_alias = "delay step" sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": 5}, "alias": delay_alias}) @@ -350,7 +306,7 @@ async def test_delay_basic(hass, mock_timeout): assert script_obj.last_action is None -async def test_multiple_runs_delay(hass, mock_timeout): +async def test_multiple_runs_delay(hass): """Test multiple runs with delay in script.""" event = "test_event" events = async_capture_events(hass, event) @@ -393,7 +349,7 @@ async def test_multiple_runs_delay(hass, mock_timeout): assert events[-1].data["value"] == 2 -async def test_delay_template_ok(hass, mock_timeout): +async def test_delay_template_ok(hass): """Test the delay as a template.""" sequence = cv.SCRIPT_SCHEMA({"delay": "00:00:{{ 5 }}"}) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") @@ -441,7 +397,7 @@ async def test_delay_template_invalid(hass, caplog): assert len(events) == 1 -async def test_delay_template_complex_ok(hass, mock_timeout): +async def test_delay_template_complex_ok(hass): """Test the delay with a working complex template.""" sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": "{{ 5 }}"}}) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") @@ -647,11 +603,56 @@ async def test_wait_template_not_schedule(hass): assert len(events) == 2 +@pytest.mark.parametrize( + "timeout_param", [5, "{{ 5 }}", {"seconds": 5}, {"seconds": "{{ 5 }}"}] +) +async def test_wait_template_timeout(hass, caplog, timeout_param): + """Test the wait timeout option.""" + event = "test_event" + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + { + "wait_template": "{{ states.switch.test.state == 'off' }}", + "timeout": timeout_param, + "continue_on_timeout": True, + }, + {"event": event}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + + try: + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(wait_started_flag.wait(), 1) + + assert script_obj.is_running + assert len(events) == 0 + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + cur_time = dt_util.utcnow() + async_fire_time_changed(hass, cur_time + timedelta(seconds=4)) + await asyncio.sleep(0) + + assert len(events) == 0 + + async_fire_time_changed(hass, cur_time + timedelta(seconds=5)) + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 1 + assert "(timeout: 0:00:05)" in caplog.text + + @pytest.mark.parametrize( "continue_on_timeout,n_events", [(False, 0), (True, 1), (None, 1)] ) -async def test_wait_template_timeout(hass, mock_timeout, continue_on_timeout, n_events): - """Test the wait template, halt on timeout.""" +async def test_wait_template_continue_on_timeout(hass, continue_on_timeout, n_events): + """Test the wait template continue_on_timeout option.""" event = "test_event" events = async_capture_events(hass, event) sequence = [ @@ -682,8 +683,8 @@ async def test_wait_template_timeout(hass, mock_timeout, continue_on_timeout, n_ assert len(events) == n_events -async def test_wait_template_variables(hass): - """Test the wait template with variables.""" +async def test_wait_template_variables_in(hass): + """Test the wait template with input variables.""" sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ is_state(data, 'off') }}"}) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") @@ -706,6 +707,58 @@ async def test_wait_template_variables(hass): assert not script_obj.is_running +@pytest.mark.parametrize("mode", ["no_timeout", "timeout_finish", "timeout_not_finish"]) +async def test_wait_template_variables_out(hass, mode): + """Test the wait template output variable.""" + event = "test_event" + events = async_capture_events(hass, event) + action = {"wait_template": "{{ states.switch.test.state == 'off' }}"} + if mode != "no_timeout": + action["timeout"] = 5 + action["continue_on_timeout"] = True + sequence = [ + action, + { + "event": event, + "event_data_template": { + "completed": "{{ wait.completed }}", + "remaining": "{{ wait.remaining }}", + }, + }, + ] + sequence = cv.SCRIPT_SCHEMA(sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + + try: + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(wait_started_flag.wait(), 1) + + assert script_obj.is_running + assert len(events) == 0 + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + if mode == "timeout_not_finish": + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + else: + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 1 + assert events[0].data["completed"] == str(mode != "timeout_not_finish") + remaining = events[0].data["remaining"] + if mode == "no_timeout": + assert remaining == "None" + elif mode == "timeout_finish": + assert 0.0 < float(remaining) < 5 + else: + assert float(remaining) == 0.0 + + async def test_condition_basic(hass): """Test if we can use conditions in a script.""" event = "test_event" From 8cf0a01149b568fd3c4cbd1c7cb5b8ffa830cc5a Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 12 Aug 2020 15:49:40 -0400 Subject: [PATCH 133/862] Add refresh_node_info command to OZW websocket api (#38573) * Add ozw refresh_node_info websocket api * Remove extra unsubs definition * Remove unused bits from refresh_node_info websocket * Add tests * Add unsubscribe test * Wait for response in unsubscribe test --- homeassistant/components/ozw/__init__.py | 17 +- homeassistant/components/ozw/const.py | 2 + homeassistant/components/ozw/manifest.json | 2 +- homeassistant/components/ozw/websocket_api.py | 241 +++++++++++------- tests/components/ozw/test_websocket_api.py | 76 +++++- 5 files changed, 242 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index ae79850d96f..f57d737bf36 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -26,7 +26,14 @@ from homeassistant.helpers.device_registry import async_get_registry as get_dev_ from homeassistant.helpers.dispatcher import async_dispatcher_send from . import const -from .const import DATA_UNSUBSCRIBE, DOMAIN, PLATFORMS, TOPIC_OPENZWAVE +from .const import ( + DATA_UNSUBSCRIBE, + DOMAIN, + MANAGER, + OPTIONS, + PLATFORMS, + TOPIC_OPENZWAVE, +) from .discovery import DISCOVERY_SCHEMAS, check_node_schema, check_value_schema from .entity import ( ZWaveDeviceEntityValues, @@ -35,7 +42,7 @@ from .entity import ( create_value_id, ) from .services import ZWaveServices -from .websocket_api import ZWaveWebsocketApi +from .websocket_api import async_register_api _LOGGER = logging.getLogger(__name__) @@ -68,6 +75,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): options = OZWOptions(send_message=send_message, topic_prefix=f"{TOPIC_OPENZWAVE}/") manager = OZWManager(options) + hass.data[DOMAIN][MANAGER] = manager + hass.data[DOMAIN][OPTIONS] = options + @callback def async_node_added(node): # Caution: This is also called on (re)start. @@ -209,8 +219,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): services.async_register() # Register WebSocket API - ws_api = ZWaveWebsocketApi(hass, manager) - ws_api.async_register_api() + async_register_api(hass) @callback def async_receive_message(msg): diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py index 93aa8da4b79..91809298382 100644 --- a/homeassistant/components/ozw/const.py +++ b/homeassistant/components/ozw/const.py @@ -20,6 +20,8 @@ PLATFORMS = [ SENSOR_DOMAIN, SWITCH_DOMAIN, ] +MANAGER = "manager" +OPTIONS = "options" # MQTT Topics TOPIC_OPENZWAVE = "OpenZWave" diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index d2cf4772bb1..191411c36ee 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -14,4 +14,4 @@ "@marcelveldt", "@MartinHjelmare" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index e7c8b047f84..1b62c892f93 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -2,11 +2,14 @@ import logging +from openzwavemqtt.const import EVENT_NODE_ADDED, EVENT_NODE_CHANGED import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import callback +from .const import DOMAIN, MANAGER, OPTIONS + _LOGGER = logging.getLogger(__name__) TYPE = "type" @@ -15,101 +18,159 @@ OZW_INSTANCE = "ozw_instance" NODE_ID = "node_id" -class ZWaveWebsocketApi: - """Class that holds our websocket api commands.""" +@callback +def async_register_api(hass): + """Register all of our api endpoints.""" + websocket_api.async_register_command(hass, websocket_network_status) + websocket_api.async_register_command(hass, websocket_node_metadata) + websocket_api.async_register_command(hass, websocket_node_status) + websocket_api.async_register_command(hass, websocket_node_statistics) + websocket_api.async_register_command(hass, websocket_refresh_node_info) - def __init__(self, hass, manager): - """Initialize with both hass and ozwmanager objects.""" - self._hass = hass - self._manager = manager + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/network_status", + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + } +) +def websocket_network_status(hass, connection, msg): + """Get Z-Wave network status.""" + + manager = hass.data[DOMAIN][MANAGER] + connection.send_result( + msg[ID], + { + "state": manager.get_instance(msg[OZW_INSTANCE]).get_status().status, + OZW_INSTANCE: msg[OZW_INSTANCE], + }, + ) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/node_status", + vol.Required(NODE_ID): vol.Coerce(int), + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + } +) +def websocket_node_status(hass, connection, msg): + """Get the status for a Z-Wave node.""" + manager = hass.data[DOMAIN][MANAGER] + node = manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]) + connection.send_result( + msg[ID], + { + "node_query_stage": node.node_query_stage, + "node_id": node.node_id, + "is_zwave_plus": node.is_zwave_plus, + "is_awake": node.is_awake, + "is_failed": node.is_failed, + "node_baud_rate": node.node_baud_rate, + "is_beaming": node.is_beaming, + "is_flirs": node.is_flirs, + "is_routing": node.is_routing, + "is_securityv1": node.is_securityv1, + "node_basic_string": node.node_basic_string, + "node_generic_string": node.node_generic_string, + "node_specific_string": node.node_specific_string, + "neighbors": node.neighbors, + OZW_INSTANCE: msg[OZW_INSTANCE], + }, + ) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/node_metadata", + vol.Required(NODE_ID): vol.Coerce(int), + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + } +) +def websocket_node_metadata(hass, connection, msg): + """Get the metadata for a Z-Wave node.""" + manager = hass.data[DOMAIN][MANAGER] + node = manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]) + connection.send_result( + msg[ID], + { + "metadata": node.meta_data, + NODE_ID: node.node_id, + OZW_INSTANCE: msg[OZW_INSTANCE], + }, + ) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/node_statistics", + vol.Required(NODE_ID): vol.Coerce(int), + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + } +) +def websocket_node_statistics(hass, connection, msg): + """Get the statistics for a Z-Wave node.""" + manager = hass.data[DOMAIN][MANAGER] + stats = ( + manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]).get_statistics() + ) + connection.send_result( + msg[ID], + { + "node_id": msg[NODE_ID], + "send_count": stats.send_count, + "sent_failed": stats.sent_failed, + "retries": stats.retries, + "last_request_rtt": stats.last_request_rtt, + "last_response_rtt": stats.last_response_rtt, + "average_request_rtt": stats.average_request_rtt, + "average_response_rtt": stats.average_response_rtt, + "received_packets": stats.received_packets, + "received_dup_packets": stats.received_dup_packets, + "received_unsolicited": stats.received_unsolicited, + OZW_INSTANCE: msg[OZW_INSTANCE], + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/refresh_node_info", + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + vol.Required(NODE_ID): vol.Coerce(int), + } +) +def websocket_refresh_node_info(hass, connection, msg): + """Tell OpenZWave to re-interview a node.""" + + manager = hass.data[DOMAIN][MANAGER] + options = hass.data[DOMAIN][OPTIONS] @callback - def async_register_api(self): - """Register all of our api endpoints.""" - websocket_api.async_register_command(self._hass, self.websocket_network_status) - websocket_api.async_register_command(self._hass, self.websocket_node_status) - websocket_api.async_register_command(self._hass, self.websocket_node_statistics) + def forward_node(node): + """Forward node events to websocket.""" + if node.node_id != msg[NODE_ID]: + return - @websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/network_status", - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + forward_data = { + "type": "node_updated", + "node_query_stage": node.node_query_stage, } - ) - def websocket_network_status(self, hass, connection, msg): - """Get Z-Wave network status.""" + connection.send_message(websocket_api.event_message(msg["id"], forward_data)) - connection.send_result( - msg[ID], - { - "state": self._manager.get_instance(msg[OZW_INSTANCE]) - .get_status() - .status, - OZW_INSTANCE: msg[OZW_INSTANCE], - }, - ) + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() - @websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/node_status", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } - ) - def websocket_node_status(self, hass, connection, msg): - """Get the status for a Z-Wave node.""" + connection.subscriptions[msg["id"]] = async_cleanup + unsubs = [ + options.listen(EVENT_NODE_CHANGED, forward_node), + options.listen(EVENT_NODE_ADDED, forward_node), + ] - node = self._manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]) - connection.send_result( - msg[ID], - { - "node_query_stage": node.node_query_stage, - "node_id": node.node_id, - "is_zwave_plus": node.is_zwave_plus, - "is_awake": node.is_awake, - "is_failed": node.is_failed, - "node_baud_rate": node.node_baud_rate, - "is_beaming": node.is_beaming, - "is_flirs": node.is_flirs, - "is_routing": node.is_routing, - "is_securityv1": node.is_securityv1, - "node_basic_string": node.node_basic_string, - "node_generic_string": node.node_generic_string, - "node_specific_string": node.node_specific_string, - "neighbors": node.neighbors, - OZW_INSTANCE: msg[OZW_INSTANCE], - }, - ) - - @websocket_api.websocket_command( - { - vol.Required(TYPE): "ozw/node_statistics", - vol.Required(NODE_ID): vol.Coerce(int), - vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), - } - ) - def websocket_node_statistics(self, hass, connection, msg): - """Get the statistics for a Z-Wave node.""" - - stats = ( - self._manager.get_instance(msg[OZW_INSTANCE]) - .get_node(msg[NODE_ID]) - .get_statistics() - ) - connection.send_result( - msg[ID], - { - "node_id": msg[NODE_ID], - "send_count": stats.send_count, - "sent_failed": stats.sent_failed, - "retries": stats.retries, - "last_request_rtt": stats.last_request_rtt, - "last_response_rtt": stats.last_response_rtt, - "average_request_rtt": stats.average_request_rtt, - "average_response_rtt": stats.average_response_rtt, - "received_packets": stats.received_packets, - "received_dup_packets": stats.received_dup_packets, - "received_unsolicited": stats.received_unsolicited, - OZW_INSTANCE: msg[OZW_INSTANCE], - }, - ) + instance = manager.get_instance(msg[OZW_INSTANCE]) + instance.refresh_node(msg[NODE_ID]) + connection.send_result(msg["id"]) diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index 13ba6f2152c..bee3a828c5a 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -2,7 +2,9 @@ from homeassistant.components.ozw.websocket_api import ID, NODE_ID, OZW_INSTANCE, TYPE -from .common import setup_ozw +from .common import MQTTMessage, setup_ozw + +from tests.async_mock import patch async def test_websocket_api(hass, generic_data, hass_ws_client): @@ -56,3 +58,75 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): assert result["received_packets"] == 3594 assert result["received_dup_packets"] == 12 assert result["received_unsolicited"] == 3546 + + # Test node metadata + await client.send_json({ID: 8, TYPE: "ozw/node_metadata", NODE_ID: 39}) + msg = await client.receive_json() + result = msg["result"] + assert result["metadata"]["ProductPic"] == "images/aeotec/zwa002.png" + + +async def test_refresh_node(hass, generic_data, sent_messages, hass_ws_client): + """Test the ozw refresh node api.""" + receive_message = await setup_ozw(hass, fixture=generic_data) + client = await hass_ws_client(hass) + + # Send the refresh_node_info command + await client.send_json({ID: 9, TYPE: "ozw/refresh_node_info", NODE_ID: 39}) + msg = await client.receive_json() + + assert len(sent_messages) == 1 + assert msg["success"] + + # Receive a mock status update from OZW + message = MQTTMessage( + topic="OpenZWave/1/node/39/", + payload={"NodeID": 39, "NodeQueryStage": "initializing"}, + ) + message.encode() + receive_message(message) + + # Verify we got expected data on the websocket + msg = await client.receive_json() + result = msg["event"] + assert result["type"] == "node_updated" + assert result["node_query_stage"] == "initializing" + + # Send another mock status update from OZW + message = MQTTMessage( + topic="OpenZWave/1/node/39/", + payload={"NodeID": 39, "NodeQueryStage": "versions"}, + ) + message.encode() + receive_message(message) + + # Send a mock status update for a different node + message = MQTTMessage( + topic="OpenZWave/1/node/35/", + payload={"NodeID": 35, "NodeQueryStage": "fake_shouldnt_be_received"}, + ) + message.encode() + receive_message(message) + + # Verify we received the message for node 39 but not for node 35 + msg = await client.receive_json() + result = msg["event"] + assert result["type"] == "node_updated" + assert result["node_query_stage"] == "versions" + + +async def test_refresh_node_unsubscribe(hass, generic_data, hass_ws_client): + """Test unsubscribing the ozw refresh node api.""" + await setup_ozw(hass, fixture=generic_data) + client = await hass_ws_client(hass) + + with patch("openzwavemqtt.OZWOptions.listen") as mock_listen: + # Send the refresh_node_info command + await client.send_json({ID: 9, TYPE: "ozw/refresh_node_info", NODE_ID: 39}) + await client.receive_json() + + # Send the unsubscribe command + await client.send_json({ID: 10, TYPE: "unsubscribe_events", "subscription": 9}) + await client.receive_json() + + assert mock_listen.return_value.called From 15db2225daa1ec30944c6391fcb40b804862cb1f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 12 Aug 2020 22:35:24 +0200 Subject: [PATCH 134/862] async_get_instance was not reentrant during await (#38263) --- homeassistant/helpers/restore_state.py | 63 ++++++++++++-------------- tests/common.py | 6 +-- tests/helpers/test_restore_state.py | 4 ++ 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index c75d9c840ed..97069913c80 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -2,7 +2,7 @@ import asyncio from datetime import datetime, timedelta import logging -from typing import Any, Awaitable, Dict, List, Optional, Set, cast +from typing import Any, Dict, List, Optional, Set, cast from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( @@ -17,6 +17,7 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.singleton import singleton from homeassistant.helpers.storage import Store import homeassistant.util.dt as dt_util @@ -63,45 +64,39 @@ class RestoreStateData: @classmethod async def async_get_instance(cls, hass: HomeAssistant) -> "RestoreStateData": """Get the singleton instance of this data helper.""" - task = hass.data.get(DATA_RESTORE_STATE_TASK) - if task is None: + @singleton(DATA_RESTORE_STATE_TASK) + async def load_instance(hass: HomeAssistant) -> "RestoreStateData": + """Get the singleton instance of this data helper.""" + data = cls(hass) - async def load_instance(hass: HomeAssistant) -> "RestoreStateData": - """Set up the restore state helper.""" - data = cls(hass) + try: + stored_states = await data.store.async_load() + except HomeAssistantError as exc: + _LOGGER.error("Error loading last states", exc_info=exc) + stored_states = None - try: - stored_states = await data.store.async_load() - except HomeAssistantError as exc: - _LOGGER.error("Error loading last states", exc_info=exc) - stored_states = None + if stored_states is None: + _LOGGER.debug("Not creating cache - no saved states found") + data.last_states = {} + else: + data.last_states = { + item["state"]["entity_id"]: StoredState.from_dict(item) + for item in stored_states + if valid_entity_id(item["state"]["entity_id"]) + } + _LOGGER.debug("Created cache with %s", list(data.last_states)) - if stored_states is None: - _LOGGER.debug("Not creating cache - no saved states found") - data.last_states = {} - else: - data.last_states = { - item["state"]["entity_id"]: StoredState.from_dict(item) - for item in stored_states - if valid_entity_id(item["state"]["entity_id"]) - } - _LOGGER.debug("Created cache with %s", list(data.last_states)) + if hass.state == CoreState.running: + data.async_setup_dump() + else: + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, data.async_setup_dump + ) - if hass.state == CoreState.running: - data.async_setup_dump() - else: - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, data.async_setup_dump - ) + return data - return data - - task = hass.data[DATA_RESTORE_STATE_TASK] = hass.async_create_task( - load_instance(hass) - ) - - return await cast(Awaitable["RestoreStateData"], task) + return cast(RestoreStateData, await load_instance(hass)) def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" diff --git a/tests/common.py b/tests/common.py index 16f349de800..e2e183061a7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -818,11 +818,7 @@ def mock_restore_cache(hass, states): _LOGGER.debug("Restore cache: %s", data.last_states) assert len(data.last_states) == len(states), f"Duplicate entity_id? {states}" - async def get_restore_state_data() -> restore_state.RestoreStateData: - return data - - # Patch the singleton task in hass.data to return our new RestoreStateData - hass.data[key] = hass.async_create_task(get_restore_state_data()) + hass.data[key] = data class MockEntity(entity.Entity): diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 7866662266d..15eed1c7e19 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -27,6 +27,7 @@ async def test_caching_data(hass): ] data = await RestoreStateData.async_get_instance(hass) + await hass.async_block_till_done() await data.store.async_save([state.as_dict() for state in stored_states]) # Emulate a fresh load @@ -41,6 +42,7 @@ async def test_caching_data(hass): "homeassistant.helpers.restore_state.Store.async_save" ) as mock_write_data: state = await entity.async_get_last_state() + await hass.async_block_till_done() assert state is not None assert state.entity_id == "input_boolean.b1" @@ -61,6 +63,7 @@ async def test_hass_starting(hass): ] data = await RestoreStateData.async_get_instance(hass) + await hass.async_block_till_done() await data.store.async_save([state.as_dict() for state in stored_states]) # Emulate a fresh load @@ -76,6 +79,7 @@ async def test_hass_starting(hass): "homeassistant.helpers.restore_state.Store.async_save" ) as mock_write_data, patch.object(hass.states, "async_all", return_value=states): state = await entity.async_get_last_state() + await hass.async_block_till_done() assert state is not None assert state.entity_id == "input_boolean.b1" From 991bf126d46f893b3c49776953c080cf8c6029dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 13 Aug 2020 00:01:10 +0300 Subject: [PATCH 135/862] Helpers type hint improvements (#38522) --- homeassistant/helpers/config_entry_flow.py | 2 -- homeassistant/helpers/entity_component.py | 2 +- homeassistant/helpers/entity_platform.py | 41 ++++++++++++++-------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index d349820978e..2b967286b95 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -5,8 +5,6 @@ from homeassistant import config_entries from .typing import HomeAssistantType -# mypy: allow-untyped-defs, no-check-untyped-defs - DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index e651d2e8cc7..e3dcbbc3c79 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -164,7 +164,7 @@ class EntityComponent: scan_interval=getattr(platform, "SCAN_INTERVAL", None), ) - return await self._platforms[key].async_setup_entry(config_entry) # type: ignore + return await self._platforms[key].async_setup_entry(config_entry) async def async_unload_entry(self, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6d9a1275b06..90e3cdb2f4d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -4,10 +4,17 @@ from contextvars import ContextVar from datetime import datetime, timedelta from logging import Logger from types import ModuleType -from typing import TYPE_CHECKING, Dict, Iterable, List, Optional +from typing import TYPE_CHECKING, Callable, Coroutine, Dict, Iterable, List, Optional +from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import CALLBACK_TYPE, callback, split_entity_id, valid_entity_id +from homeassistant.core import ( + CALLBACK_TYPE, + ServiceCall, + callback, + split_entity_id, + valid_entity_id, +) from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import config_validation as cv, service from homeassistant.helpers.typing import HomeAssistantType @@ -19,7 +26,7 @@ from .event import async_call_later, async_track_time_interval if TYPE_CHECKING: from .entity import Entity -# mypy: allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-defs SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 @@ -53,7 +60,7 @@ class EntityPlatform: self.platform = platform self.scan_interval = scan_interval self.entity_namespace = entity_namespace - self.config_entry = None + self.config_entry: Optional[ConfigEntry] = None self.entities: Dict[str, Entity] = {} # pylint: disable=used-before-assignment self._tasks: List[asyncio.Future] = [] # Method to cancel the state change listener @@ -119,10 +126,10 @@ class EntityPlatform: return @callback - def async_create_setup_task(): + def async_create_setup_task() -> Coroutine: """Get task to set up platform.""" if getattr(platform, "async_setup_platform", None): - return platform.async_setup_platform( + return platform.async_setup_platform( # type: ignore hass, platform_config, self._async_schedule_add_entities, @@ -133,7 +140,7 @@ class EntityPlatform: # we don't want to track this task in case it blocks startup. return hass.loop.run_in_executor( None, - platform.setup_platform, + platform.setup_platform, # type: ignore hass, platform_config, self._schedule_add_entities, @@ -142,7 +149,7 @@ class EntityPlatform: await self._async_setup_platform(async_create_setup_task) - async def async_setup_entry(self, config_entry): + async def async_setup_entry(self, config_entry: ConfigEntry) -> bool: """Set up the platform from a config entry.""" # Store it so that we can save config entry ID in entity registry self.config_entry = config_entry @@ -151,13 +158,15 @@ class EntityPlatform: @callback def async_create_setup_task(): """Get task to set up platform.""" - return platform.async_setup_entry( + return platform.async_setup_entry( # type: ignore self.hass, config_entry, self._async_schedule_add_entities ) return await self._async_setup_platform(async_create_setup_task) - async def _async_setup_platform(self, async_create_setup_task, tries=0): + async def _async_setup_platform( + self, async_create_setup_task: Callable[[], Coroutine], tries: int = 0 + ) -> bool: """Set up a platform via config file or config entry. async_create_setup_task creates a coroutine that sets up platform. @@ -340,7 +349,7 @@ class EntityPlatform: return requested_entity_id = None - suggested_object_id = None + suggested_object_id: Optional[str] = None # Get entity_id from unique ID registration if entity.unique_id is not None: @@ -354,7 +363,7 @@ class EntityPlatform: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" if self.config_entry is not None: - config_entry_id = self.config_entry.entry_id + config_entry_id: Optional[str] = self.config_entry.entry_id else: config_entry_id = None @@ -512,7 +521,9 @@ class EntityPlatform: self._async_unsub_polling() self._async_unsub_polling = None - async def async_extract_from_service(self, service_call, expand_group=True): + async def async_extract_from_service( + self, service_call: ServiceCall, expand_group: bool = True + ) -> List["Entity"]: """Extract all known and available entities from a service call. Will return an empty list if entities specified but unknown. @@ -535,9 +546,9 @@ class EntityPlatform: if isinstance(schema, dict): schema = cv.make_entity_service_schema(schema) - async def handle_service(call): + async def handle_service(call: ServiceCall) -> None: """Handle the service.""" - await service.entity_service_call( + await service.entity_service_call( # type: ignore self.hass, [ plf From 9e21fb6b5245c96a64e760ff5e33496bfb2de762 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 12 Aug 2020 15:21:17 -0600 Subject: [PATCH 136/862] Handle unhandled exceptions related to unavailable SimpliSafe features (#38812) --- homeassistant/components/simplisafe/__init__.py | 9 ++++++++- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 327549eeb62..07b4942ad34 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -3,7 +3,7 @@ import asyncio from uuid import UUID from simplipy import API -from simplipy.errors import InvalidCredentialsError, SimplipyError +from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError from simplipy.websocket import ( EVENT_CAMERA_MOTION_DETECTED, EVENT_CONNECTION_LOST, @@ -555,6 +555,13 @@ class SimpliSafe: LOGGER.error("Error while using stored refresh token: %s", err) return + if isinstance(result, EndpointUnavailable): + # In case the user attempt an action not allowed in their current plan, + # we merely log that message at INFO level (so the user is aware, + # but not spammed with ERROR messages that they cannot change): + LOGGER.info(result) + return + if isinstance(result, SimplipyError): LOGGER.error("SimpliSafe error while updating: %s", result) return diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 0ec77d13b9a..dd9ab53cb98 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.2.2"], + "requirements": ["simplisafe-python==9.3.0"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 93cf481ec4d..64209670b6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1975,7 +1975,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.2.2 +simplisafe-python==9.3.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed62aeee367..83b6a323472 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -893,7 +893,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.2.2 +simplisafe-python==9.3.0 # homeassistant.components.sleepiq sleepyq==0.7 From 40f31faa106b82600607f321c54b872c5347599d Mon Sep 17 00:00:00 2001 From: Kit Klein <33464407+kit-klein@users.noreply.github.com> Date: Wed, 12 Aug 2020 18:37:18 -0400 Subject: [PATCH 137/862] update to use latest konnected py module (#38803) --- homeassistant/components/konnected/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index 95c14050a72..b6c1c8117fb 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -3,7 +3,7 @@ "name": "Konnected.io", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/konnected", - "requirements": ["konnected==1.1.0"], + "requirements": ["konnected==1.2.0"], "ssdp": [ { "manufacturer": "konnected.io" diff --git a/requirements_all.txt b/requirements_all.txt index 64209670b6f..b0eefabb78a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -827,7 +827,7 @@ keyrings.alt==3.4.0 kiwiki-client==0.1.1 # homeassistant.components.konnected -konnected==1.1.0 +konnected==1.2.0 # homeassistant.components.eufy lakeside==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83b6a323472..4a0fa6254d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -406,7 +406,7 @@ keyring==21.2.0 keyrings.alt==3.4.0 # homeassistant.components.konnected -konnected==1.1.0 +konnected==1.2.0 # homeassistant.components.dyson libpurecool==0.6.3 From 4d50a20500e693dd13b0247a4b5f86ddb75e4473 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 12 Aug 2020 20:40:32 -0500 Subject: [PATCH 138/862] Update pyipp to 0.11.0 (#38820) * update pyipp to 0.11.0 * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 216ee519a3a..d4e3669b795 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -2,7 +2,7 @@ "domain": "ipp", "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", - "requirements": ["pyipp==0.10.1"], + "requirements": ["pyipp==0.11.0"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index b0eefabb78a..8fe930ad5cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1413,7 +1413,7 @@ pyintesishome==1.7.5 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.10.1 +pyipp==0.11.0 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a0fa6254d8..c866c68aa92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -671,7 +671,7 @@ pyinsteon==1.0.7 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.10.1 +pyipp==0.11.0 # homeassistant.components.iqvia pyiqvia==0.2.1 From 851c20aeb2c9947a10d17d7b31213705ed01ca07 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 12 Aug 2020 20:41:29 -0500 Subject: [PATCH 139/862] Update rokuecp to 0.6.0 (#38819) * update rokuecp to 0.6.0 * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index a5bed4530a8..39b48b91a84 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.5.0"], + "requirements": ["rokuecp==0.6.0"], "ssdp": [ { "st": "roku:ecp", diff --git a/requirements_all.txt b/requirements_all.txt index 8fe930ad5cd..66971ebbe57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1909,7 +1909,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.5.0 +rokuecp==0.6.0 # homeassistant.components.roomba roombapy==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c866c68aa92..393a99e5e1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -866,7 +866,7 @@ rflink==0.0.52 ring_doorbell==0.6.0 # homeassistant.components.roku -rokuecp==0.5.0 +rokuecp==0.6.0 # homeassistant.components.roomba roombapy==1.6.1 From 9244bf28efec15e66c7a200b77d63ed5c764c257 Mon Sep 17 00:00:00 2001 From: sean tearney Date: Thu, 13 Aug 2020 13:28:06 +0800 Subject: [PATCH 140/862] Add Agent DVR Alarm Control Panel (#36468) * Add Agent DVR Alarm Control Panel * code review * remove return statement --- .coveragerc | 1 + .../components/agent_dvr/__init__.py | 2 +- .../agent_dvr/alarm_control_panel.py | 124 ++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/agent_dvr/alarm_control_panel.py diff --git a/.coveragerc b/.coveragerc index e6fcfea3543..41b7b8dcb88 100644 --- a/.coveragerc +++ b/.coveragerc @@ -25,6 +25,7 @@ omit = homeassistant/components/ads/* homeassistant/components/aftership/sensor.py homeassistant/components/agent_dvr/__init__.py + homeassistant/components/agent_dvr/alarm_control_panel.py homeassistant/components/agent_dvr/camera.py homeassistant/components/agent_dvr/const.py homeassistant/components/agent_dvr/helpers.py diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index e11e61a4126..cc72b1e33ae 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -16,7 +16,7 @@ DEFAULT_BRAND = "Agent DVR by ispyconnect.com" _LOGGER = logging.getLogger(__name__) -FORWARDS = ["camera"] +FORWARDS = ["alarm_control_panel", "camera"] async def async_setup(hass, config): diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py new file mode 100644 index 00000000000..3e093ae46a8 --- /dev/null +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -0,0 +1,124 @@ +"""Support for Agent DVR Alarm Control Panels.""" +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, +) + +from .const import CONNECTION, DOMAIN as AGENT_DOMAIN + +ICON = "mdi:security" + +CONF_HOME_MODE_NAME = "home" +CONF_AWAY_MODE_NAME = "away" +CONF_NIGHT_MODE_NAME = "night" + +CONST_ALARM_CONTROL_PANEL_NAME = "Alarm Panel" + + +async def async_setup_entry( + hass, config_entry, async_add_entities, discovery_info=None +): + """Set up the Agent DVR Alarm Control Panels.""" + async_add_entities( + [AgentBaseStation(hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION])] + ) + + +class AgentBaseStation(AlarmControlPanelEntity): + """Representation of an Agent DVR Alarm Control Panel.""" + + def __init__(self, client): + """Initialize the alarm control panel.""" + self._state = None + self._client = client + self._unique_id = f"{client.unique}_CP" + name = CONST_ALARM_CONTROL_PANEL_NAME + self._name = name = f"{client.name} {name}" + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + + @property + def device_info(self): + """Return the device info for adding the entity to the agent object.""" + return { + "identifiers": {(AGENT_DOMAIN, self._client.unique)}, + "manufacturer": "Agent", + "model": CONST_ALARM_CONTROL_PANEL_NAME, + "sw_version": self._client.version, + } + + async def async_update(self): + """Update the state of the device.""" + await self._client.update() + armed = self._client.is_armed + if armed is None: + self._state = None + return + if armed: + prof = (await self._client.get_active_profile()).lower() + self._state = STATE_ALARM_ARMED_AWAY + if prof == CONF_HOME_MODE_NAME: + self._state = STATE_ALARM_ARMED_HOME + elif prof == CONF_NIGHT_MODE_NAME: + self._state = STATE_ALARM_ARMED_NIGHT + else: + self._state = STATE_ALARM_DISARMED + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + await self._client.disarm() + self._state = STATE_ALARM_DISARMED + + async def async_alarm_arm_away(self, code=None): + """Send arm away command. Uses custom mode.""" + await self._client.arm() + await self._client.set_active_profile(CONF_AWAY_MODE_NAME) + self._state = STATE_ALARM_ARMED_AWAY + + async def async_alarm_arm_home(self, code=None): + """Send arm home command. Uses custom mode.""" + await self._client.arm() + await self._client.set_active_profile(CONF_HOME_MODE_NAME) + self._state = STATE_ALARM_ARMED_HOME + + async def async_alarm_arm_night(self, code=None): + """Send arm night command. Uses custom mode.""" + await self._client.arm() + await self._client.set_active_profile(CONF_NIGHT_MODE_NAME) + self._state = STATE_ALARM_ARMED_NIGHT + + @property + def name(self): + """Return the name of the base station.""" + return self._name + + @property + def available(self) -> bool: + """Device available.""" + return self._client.is_available + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id From 3957337b9f7119119e05fd8f0633c5221d5e65ec Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 13 Aug 2020 09:36:47 +0200 Subject: [PATCH 141/862] Cleanup Netatmo sensors (#38627) --- homeassistant/components/netatmo/sensor.py | 91 +++++++++++++--------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 2352b4abee8..2c55e76df79 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -6,9 +6,13 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, CONCENTRATION_PARTS_PER_MILLION, + DEGREE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + PRESSURE_MBAR, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, UNIT_PERCENTAGE, @@ -50,7 +54,7 @@ SENSOR_TYPES = { DEVICE_CLASS_TEMPERATURE, ], "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2", None], - "pressure": ["Pressure", "mbar", "mdi:gauge", None], + "pressure": ["Pressure", PRESSURE_MBAR, "mdi:gauge", DEVICE_CLASS_PRESSURE], "noise": ["Noise", "dB", "mdi:volume-high", None], "humidity": [ "Humidity", @@ -64,30 +68,40 @@ SENSOR_TYPES = { "battery_vp": ["Battery", "", "mdi:battery", None], "battery_lvl": ["Battery Level", "", "mdi:battery", None], "battery_percent": ["Battery Percent", UNIT_PERCENTAGE, None, DEVICE_CLASS_BATTERY], - "min_temp": ["Min Temp.", TEMP_CELSIUS, "mdi:thermometer", None], - "max_temp": ["Max Temp.", TEMP_CELSIUS, "mdi:thermometer", None], - "windangle": ["Angle", "", "mdi:compass", None], - "windangle_value": ["Angle Value", "º", "mdi:compass", None], + "min_temp": [ + "Min Temp.", + TEMP_CELSIUS, + "mdi:thermometer", + DEVICE_CLASS_TEMPERATURE, + ], + "max_temp": [ + "Max Temp.", + TEMP_CELSIUS, + "mdi:thermometer", + DEVICE_CLASS_TEMPERATURE, + ], + "windangle": ["Angle", None, "mdi:compass-outline", None], + "windangle_value": ["Angle Value", DEGREE, "mdi:compass-outline", None], "windstrength": [ "Wind Strength", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy", None, ], - "gustangle": ["Gust Angle", "", "mdi:compass", None], - "gustangle_value": ["Gust Angle Value", "º", "mdi:compass", None], + "gustangle": ["Gust Angle", None, "mdi:compass-outline", None], + "gustangle_value": ["Gust Angle Value", DEGREE, "mdi:compass-outline", None], "guststrength": [ "Gust Strength", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy", None, ], - "reachable": ["Reachability", "", "mdi:signal", None], - "rf_status": ["Radio", "", "mdi:signal", None], - "rf_status_lvl": ["Radio Level", "", "mdi:signal", None], - "wifi_status": ["Wifi", "", "mdi:wifi", None], - "wifi_status_lvl": ["Wifi Level", "dBm", "mdi:wifi", None], - "health_idx": ["Health", "", "mdi:cloud", None], + "reachable": ["Reachability", None, "mdi:signal", None], + "rf_status": ["Radio", None, "mdi:signal", None], + "rf_status_lvl": ["Radio Level", "", "mdi:signal", DEVICE_CLASS_SIGNAL_STRENGTH], + "wifi_status": ["Wifi", None, "mdi:wifi", None], + "wifi_status_lvl": ["Wifi Level", "dBm", "mdi:wifi", DEVICE_CLASS_SIGNAL_STRENGTH], + "health_idx": ["Health", None, "mdi:cloud", None], } MODULE_TYPE_OUTDOOR = "NAModule1" @@ -107,7 +121,6 @@ PUBLIC = "public" async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" - device_registry = await hass.helpers.device_registry.async_get_registry() data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] async def find_entities(data_class_name): @@ -137,13 +150,21 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug( "Adding module %s %s", module.get("module_name"), module.get("_id"), ) - for condition in data_class.get_monitored_conditions( - module_id=module["_id"] - ): + conditions = [ + c.lower() + for c in data_class.get_monitored_conditions(module_id=module["_id"]) + ] + for condition in conditions: + if f"{condition}_value" in SENSOR_TYPES: + conditions.append(f"{condition}_value") + elif f"{condition}_lvl" in SENSOR_TYPES: + conditions.append(f"{condition}_lvl") + elif condition == "battery_vp": + conditions.append("battery_lvl") + + for condition in conditions: entities.append( - NetatmoSensor( - data_handler, data_class_name, module, condition.lower() - ) + NetatmoSensor(data_handler, data_class_name, module, condition) ) return entities @@ -154,6 +175,8 @@ async def async_setup_entry(hass, entry, async_add_entities): ]: async_add_entities(await find_entities(data_class_name), True) + device_registry = await hass.helpers.device_registry.async_get_registry() + @callback async def add_public_entities(update=True): """Retrieve Netatmo public weather entities.""" @@ -214,11 +237,6 @@ async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Netatmo weather and homecoach platform.""" - return - - class NetatmoSensor(NetatmoBase): """Implementation of a Netatmo sensor.""" @@ -298,8 +316,8 @@ class NetatmoSensor(NetatmoBase): if data is None: if self._state: _LOGGER.debug( - "No data (%s) found for %s (%s)", - self._data, + "No data found for %s - %s (%s)", + self.name, self._device_name, self._id, ) @@ -367,22 +385,22 @@ class NetatmoSensor(NetatmoBase): def process_angle(angle: int) -> str: """Process angle and return string for display.""" if angle >= 330: - return f"N ({angle}\xb0)" + return "N" if angle >= 300: - return f"NW ({angle}\xb0)" + return "NW" if angle >= 240: - return f"W ({angle}\xb0)" + return "W" if angle >= 210: - return f"SW ({angle}\xb0)" + return "SW" if angle >= 150: - return f"S ({angle}\xb0)" + return "S" if angle >= 120: - return f"SE ({angle}\xb0)" + return "SE" if angle >= 60: - return f"E ({angle}\xb0)" + return "E" if angle >= 30: - return f"NE ({angle}\xb0)" - return f"N ({angle}\xb0)" + return "NE" + return "N" def process_battery(data: int, model: str) -> str: @@ -524,7 +542,6 @@ class NetatmoPublicSensor(NetatmoBase): ) ) - @callback async def async_config_update_callback(self, area): """Update the entity's config.""" if self.area == area: From a6cec21c435d8fda5d9b290426967b6bf9372399 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Aug 2020 03:12:18 -0500 Subject: [PATCH 142/862] Make executor max_workers consistent between python versions (#38821) The default on python 3.8 is for max_workers is significantly lower than the default on python 3.7 which means we can get starved for workers. To determine a reasonable maximum, the maximum was increased to large number on 5 production instances. The number of worker threads created during startup that were needed to avoid waiting for a thread: HOU 1 - 71 HOU 2 - 48 OGG 1 - 60 OGG 2 - 68 OGG 3 - 64 This lead to a selection of 64 as it was reliable in all cases and did not have a significant memory impact --- homeassistant/runner.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 26e7bab7616..b397f9438f2 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -11,6 +11,18 @@ from homeassistant import bootstrap from homeassistant.core import callback from homeassistant.helpers.frame import warn_use +# +# Python 3.8 has significantly less workers by default +# than Python 3.7. In order to be consistent between +# supported versions, we need to set max_workers. +# +# In most cases the workers are not I/O bound, as they +# are sleeping/blocking waiting for data from integrations +# updating so this number should be higher than the default +# use case. +# +MAX_EXECUTOR_WORKERS = 64 + @dataclasses.dataclass class RuntimeConfig: @@ -57,7 +69,9 @@ class HassEventLoopPolicy(PolicyBase): # type: ignore if self.debug: loop.set_debug(True) - executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker") + executor = ThreadPoolExecutor( + thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS + ) loop.set_default_executor(executor) loop.set_default_executor = warn_use( # type: ignore loop.set_default_executor, "sets default executor on the event loop" From b3571602bb70054142f3da8f45515e2e3da7629f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Aug 2020 10:38:56 +0200 Subject: [PATCH 143/862] Add default_* to device registry (#38829) --- .../components/mikrotik/device_tracker.py | 8 ++--- homeassistant/helpers/device_registry.py | 13 ++++++++ homeassistant/helpers/entity_platform.py | 3 ++ tests/helpers/test_device_registry.py | 32 +++++++++++++++++++ tests/helpers/test_entity_platform.py | 8 ++++- 5 files changed, 59 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index e7c5e5655a0..01075a5f2d8 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from .const import ATTR_MANUFACTURER, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -115,10 +115,10 @@ class MikrotikHubTracker(ScannerEntity): """Return a client description for device registry.""" info = { "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, - "manufacturer": ATTR_MANUFACTURER, "identifiers": {(DOMAIN, self.device.mac)}, - "name": self.name, - "via_device": (DOMAIN, self.hub.serial_num), + # We only get generic info from device discovery and so don't want + # to override API specific info that integrations can provide + "default_name": self.name, } return info diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index b2e3bfd7a32..a52a3837868 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -209,6 +209,9 @@ class DeviceRegistry: manufacturer=_UNDEF, model=_UNDEF, name=_UNDEF, + default_manufacturer=_UNDEF, + default_model=_UNDEF, + default_name=_UNDEF, sw_version=_UNDEF, entry_type=_UNDEF, via_device=None, @@ -236,6 +239,16 @@ class DeviceRegistry: device = deleted_device.to_device_entry() self._add_device(device) + else: + if default_manufacturer and not device.manufacturer: + manufacturer = default_manufacturer + + if default_model and not device.model: + model = default_model + + if default_name and not device.name: + name = default_name + if via_device is not None: via = self.async_get_device({via_device}, set()) via_device_id = via.id if via else _UNDEF diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 90e3cdb2f4d..0ca2010df48 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -378,6 +378,9 @@ class EntityPlatform: "manufacturer", "model", "name", + "default_manufacturer", + "default_model", + "default_name", "sw_version", "entry_type", "via_device", diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 181a012807a..5b671dce134 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -839,3 +839,35 @@ async def test_restore_simple_device(hass, registry, update_events): assert update_events[2]["device_id"] == entry2.id assert update_events[3]["action"] == "create" assert update_events[3]["device_id"] == entry3.id + + +async def test_get_or_create_sets_default_values(hass, registry): + """Make sure we do not duplicate entries.""" + entry = registry.async_get_or_create( + identifiers={("bridgeid", "0123")}, config_entry_id="1234" + ) + assert entry.name is None + assert entry.model is None + assert entry.manufacturer is None + + entry = registry.async_get_or_create( + config_entry_id="1234", + identifiers={("bridgeid", "0123")}, + default_name="default name 1", + default_model="default model 1", + default_manufacturer="default manufacturer 1", + ) + assert entry.name == "default name 1" + assert entry.model == "default model 1" + assert entry.manufacturer == "default manufacturer 1" + + entry = registry.async_get_or_create( + config_entry_id="1234", + identifiers={("bridgeid", "0123")}, + default_name="default name 2", + default_model="default model 2", + default_manufacturer="default manufacturer 2", + ) + assert entry.name == "default name 1" + assert entry.model == "default model 1" + assert entry.manufacturer == "default manufacturer 1" diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 6d03b087151..43939f44e7e 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -777,7 +777,13 @@ async def test_device_info_not_overrides(hass): async_add_entities( [ MockEntity( - unique_id="qwer", device_info={"connections": {("mac", "abcd")}} + unique_id="qwer", + device_info={ + "connections": {("mac", "abcd")}, + "default_name": "default name 1", + "default_model": "default model 1", + "default_manufacturer": "default manufacturer 1", + }, ) ] ) From 7343649c549f3a3e96f7025461f1e9f33a67dd31 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Aug 2020 11:08:59 +0200 Subject: [PATCH 144/862] Convert Channels platform services to use platform register (#38827) --- .../components/channels/media_player.py | 59 ++++--------------- 1 file changed, 11 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 65be051ad17..78286b70145 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -20,7 +20,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, ) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, @@ -28,9 +27,9 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_platform -from .const import DOMAIN, SERVICE_SEEK_BACKWARD, SERVICE_SEEK_BY, SERVICE_SEEK_FORWARD +from .const import SERVICE_SEEK_BACKWARD, SERVICE_SEEK_BY, SERVICE_SEEK_FORWARD _LOGGER = logging.getLogger(__name__) @@ -61,58 +60,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( # Service call validation schemas ATTR_SECONDS = "seconds" -CHANNELS_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id}) -CHANNELS_SEEK_BY_SCHEMA = CHANNELS_SCHEMA.extend( - {vol.Required(ATTR_SECONDS): vol.Coerce(int)} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Channels platform.""" device = ChannelsPlayer(config[CONF_NAME], config[CONF_HOST], config[CONF_PORT]) + async_add_entities([device], True) - if DATA_CHANNELS not in hass.data: - hass.data[DATA_CHANNELS] = [] + platform = entity_platform.current_platform.get() - add_entities([device], True) - hass.data[DATA_CHANNELS].append(device) - - def service_handler(service): - """Handle service.""" - entity_id = service.data.get(ATTR_ENTITY_ID) - - device = next( - ( - device - for device in hass.data[DATA_CHANNELS] - if device.entity_id == entity_id - ), - None, - ) - - if device is None: - _LOGGER.warning("Unable to find Channels with entity_id: %s", entity_id) - return - - if service.service == SERVICE_SEEK_FORWARD: - device.seek_forward() - elif service.service == SERVICE_SEEK_BACKWARD: - device.seek_backward() - elif service.service == SERVICE_SEEK_BY: - seconds = service.data.get("seconds") - device.seek_by(seconds) - - hass.services.register( - DOMAIN, SERVICE_SEEK_FORWARD, service_handler, schema=CHANNELS_SCHEMA + platform.async_register_entity_service( + SERVICE_SEEK_FORWARD, {}, "seek_forward", ) - - hass.services.register( - DOMAIN, SERVICE_SEEK_BACKWARD, service_handler, schema=CHANNELS_SCHEMA + platform.async_register_entity_service( + SERVICE_SEEK_BACKWARD, {}, "seek_backward", ) - - hass.services.register( - DOMAIN, SERVICE_SEEK_BY, service_handler, schema=CHANNELS_SEEK_BY_SCHEMA + platform.async_register_entity_service( + SERVICE_SEEK_BY, {vol.Required(ATTR_SECONDS): vol.Coerce(int)}, "seek_by", ) From 1f3b9bc70cc5e6c7db1522933c2182968d2e2162 Mon Sep 17 00:00:00 2001 From: RogerSelwyn Date: Thu, 13 Aug 2020 10:47:32 +0100 Subject: [PATCH 145/862] Fix creation of unrequired sensors in OVO energy (#38835) --- homeassistant/components/ovo_energy/sensor.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 5fe1bb056e7..4b9e2e70806 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -27,18 +27,20 @@ async def async_setup_entry( ] client: OVOEnergy = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] - currency = coordinator.data.electricity[ - len(coordinator.data.electricity) - 1 - ].cost.currency_unit + entities = [] + + if coordinator.data.electricity: + currency = coordinator.data.electricity[ + len(coordinator.data.electricity) - 1 + ].cost.currency_unit + entities.append(OVOEnergyLastElectricityReading(coordinator, client)) + entities.append(OVOEnergyLastElectricityCost(coordinator, client, currency)) + if coordinator.data.gas: + entities.append(OVOEnergyLastGasReading(coordinator, client)) + entities.append(OVOEnergyLastGasCost(coordinator, client, currency)) async_add_entities( - [ - OVOEnergyLastElectricityReading(coordinator, client), - OVOEnergyLastGasReading(coordinator, client), - OVOEnergyLastElectricityCost(coordinator, client, currency), - OVOEnergyLastGasCost(coordinator, client, currency), - ], - True, + entities, True, ) From 03676693ceed87582527a0c5bce1cb615779e30a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 13 Aug 2020 12:11:58 +0200 Subject: [PATCH 146/862] Catch upnp timeout error (#38794) --- homeassistant/components/upnp/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 98bf3e6f4dd..3a34cb26604 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,4 +1,5 @@ """Open ports in your router for Home Assistant and provide statistics.""" +import asyncio from ipaddress import ip_address from operator import itemgetter @@ -106,7 +107,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) # discover and construct udn = config_entry.data.get(CONFIG_ENTRY_UDN) st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name - device = await async_discover_and_construct(hass, udn, st) + try: + device = await async_discover_and_construct(hass, udn, st) + except asyncio.TimeoutError: + raise ConfigEntryNotReady + if not device: _LOGGER.info("Unable to create UPnP/IGD, aborting") raise ConfigEntryNotReady From 86aa758ecd18a5ba7aac21e6e53390736829677c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 13 Aug 2020 07:26:47 -0400 Subject: [PATCH 147/862] Add binary sensor support to the Flo integration (#38267) * update device * add binary sensor * updates post rebase * fix entity type post rebase * fix post rebase * fix add entities * fix name * review comments --- homeassistant/components/flo/__init__.py | 2 +- homeassistant/components/flo/binary_sensor.py | 49 +++++++++++++++++++ homeassistant/components/flo/device.py | 24 +++++++++ tests/components/flo/test_binary_sensor.py | 30 ++++++++++++ tests/components/flo/test_device.py | 4 ++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/flo/binary_sensor.py create mode 100644 tests/components/flo/test_binary_sensor.py diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 2c267addb0c..b02a848de7c 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -19,7 +19,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = ["binary_sensor", "sensor"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py new file mode 100644 index 00000000000..09facae53ed --- /dev/null +++ b/homeassistant/components/flo/binary_sensor.py @@ -0,0 +1,49 @@ +"""Support for Flo Water Monitor binary sensors.""" + +from typing import List + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) + +from .const import DOMAIN as FLO_DOMAIN +from .device import FloDeviceDataUpdateCoordinator +from .entity import FloEntity + +DEPENDENCIES = ["flo"] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Flo sensors from config entry.""" + devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN]["devices"] + entities = [FloPendingAlertsBinarySensor(device) for device in devices] + async_add_entities(entities) + + +class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): + """Binary sensor that reports on if there are any pending system alerts.""" + + def __init__(self, device): + """Initialize the pending alerts binary sensor.""" + super().__init__("pending_system_alerts", "Pending System Alerts", device) + + @property + def is_on(self): + """Return true if the Flo device has pending alerts.""" + return self._device.has_alerts + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + if self._device.has_alerts: + attr["info"] = self._device.pending_info_alerts_count + attr["warning"] = self._device.pending_warning_alerts_count + attr["critical"] = self._device.pending_critical_alerts_count + return attr + + @property + def device_class(self): + """Return the device class for the binary sensor.""" + return DEVICE_CLASS_PROBLEM diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index ad7973f4c9a..abba045693d 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -138,6 +138,30 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): """Return the serial number for the device.""" return self._device_information["serialNumber"] + @property + def pending_info_alerts_count(self) -> int: + """Return the number of pending info alerts for the device.""" + return self._device_information["notifications"]["pending"]["infoCount"] + + @property + def pending_warning_alerts_count(self) -> int: + """Return the number of pending warning alerts for the device.""" + return self._device_information["notifications"]["pending"]["warningCount"] + + @property + def pending_critical_alerts_count(self) -> int: + """Return the number of pending critical alerts for the device.""" + return self._device_information["notifications"]["pending"]["criticalCount"] + + @property + def has_alerts(self) -> bool: + """Return True if any alert counts are greater than zero.""" + return bool( + self.pending_info_alerts_count + or self.pending_warning_alerts_count + or self.pending_warning_alerts_count + ) + async def _update_device(self, *_) -> None: """Update the device information from the API.""" self._device_information = await self.api_client.device.get_info( diff --git a/tests/components/flo/test_binary_sensor.py b/tests/components/flo/test_binary_sensor.py new file mode 100644 index 00000000000..9f727c5a10b --- /dev/null +++ b/tests/components/flo/test_binary_sensor.py @@ -0,0 +1,30 @@ +"""Test Flo by Moen binary sensor entities.""" +from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + CONF_PASSWORD, + CONF_USERNAME, + STATE_ON, +) +from homeassistant.setup import async_setup_component + +from .common import TEST_PASSWORD, TEST_USER_ID + + +async def test_binary_sensors(hass, config_entry, aioclient_mock_fixture): + """Test Flo by Moen sensors.""" + config_entry.add_to_hass(hass) + assert await async_setup_component( + hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} + ) + await hass.async_block_till_done() + + assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + + # we should have 6 entities for the device + state = hass.states.get("binary_sensor.pending_system_alerts") + assert state.state == STATE_ON + assert state.attributes.get("info") == 0 + assert state.attributes.get("warning") == 2 + assert state.attributes.get("critical") == 0 + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending System Alerts" diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py index 13f6cd5293a..5f5a035ecbd 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -41,6 +41,10 @@ async def test_device(hass, config_entry, aioclient_mock_fixture, aioclient_mock assert device.manufacturer == "Flo by Moen" assert device.device_name == "Flo by Moen flo_device_075_v2" assert device.rssi == -47 + assert device.pending_info_alerts_count == 0 + assert device.pending_critical_alerts_count == 0 + assert device.pending_warning_alerts_count == 2 + assert device.has_alerts is True call_count = aioclient_mock.call_count From 52a9921ed35b9c8e2b8741141cf3352416fdd6db Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Thu, 13 Aug 2020 08:46:07 -0300 Subject: [PATCH 148/862] Nightscout PR fixes (#38737) * Don't allow duplicate nightscout configs * Fix nightscout translations * Remove unnecessary should_poll method * Remove SVG attribute, as it was duplicating the state * Use aiohttp client session from HA * Move validate_input outside the config class * Use the entry unique_id on the sensor * Move create entity logic * Handle unexpected exception on Nightscout config --- .../components/nightscout/__init__.py | 5 ++- .../components/nightscout/config_flow.py | 24 ++++++++--- homeassistant/components/nightscout/const.py | 1 - homeassistant/components/nightscout/sensor.py | 15 ++----- .../components/nightscout/strings.json | 3 ++ .../nightscout/translations/ca.json | 15 ------- .../nightscout/translations/de.json | 15 ------- .../nightscout/translations/en.json | 10 +++-- .../nightscout/translations/es.json | 15 ------- .../nightscout/translations/fr.json | 15 ------- .../nightscout/translations/it.json | 15 ------- .../nightscout/translations/ko.json | 15 ------- .../nightscout/translations/lb.json | 14 ------ .../nightscout/translations/pl.json | 15 ------- .../nightscout/translations/pt.json | 15 ------- .../nightscout/translations/ru.json | 15 ------- .../nightscout/translations/sl.json | 15 ------- .../nightscout/translations/zh-Hant.json | 15 ------- homeassistant/components/nightscout/utils.py | 7 +++ .../components/nightscout/test_config_flow.py | 43 ++++++++++++++----- tests/components/nightscout/test_sensor.py | 2 - 21 files changed, 74 insertions(+), 215 deletions(-) delete mode 100644 homeassistant/components/nightscout/translations/ca.json delete mode 100644 homeassistant/components/nightscout/translations/de.json delete mode 100644 homeassistant/components/nightscout/translations/es.json delete mode 100644 homeassistant/components/nightscout/translations/fr.json delete mode 100644 homeassistant/components/nightscout/translations/it.json delete mode 100644 homeassistant/components/nightscout/translations/ko.json delete mode 100644 homeassistant/components/nightscout/translations/lb.json delete mode 100644 homeassistant/components/nightscout/translations/pl.json delete mode 100644 homeassistant/components/nightscout/translations/pt.json delete mode 100644 homeassistant/components/nightscout/translations/ru.json delete mode 100644 homeassistant/components/nightscout/translations/sl.json delete mode 100644 homeassistant/components/nightscout/translations/zh-Hant.json create mode 100644 homeassistant/components/nightscout/utils.py diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 91cf056689c..88939cbe790 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import SLOW_UPDATE_WARNING from .const import DOMAIN @@ -29,8 +30,8 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Nightscout from a config entry.""" server_url = entry.data[CONF_URL] - - api = NightscoutAPI(server_url) + session = async_get_clientsession(hass) + api = NightscoutAPI(server_url, session=session) try: status = await api.get_server_status() except (ClientError, AsyncIOTimeoutError, OSError) as error: diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py index c54293798ea..699ce39355f 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -10,6 +10,7 @@ from homeassistant import config_entries, exceptions from homeassistant.const import CONF_URL from .const import DOMAIN # pylint:disable=unused-import +from .utils import hash_from_url _LOGGER = logging.getLogger(__name__) @@ -18,12 +19,12 @@ DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL): str}) async def _validate_input(data): """Validate the user input allows us to connect.""" - + url = data[CONF_URL] try: - api = NightscoutAPI(data[CONF_URL]) + api = NightscoutAPI(url) status = await api.get_server_status() except (ClientError, AsyncIOTimeoutError, OSError): - raise CannotConnect + raise InputValidationError("cannot_connect") # Return info to be stored in the config entry. return {"title": status.name} @@ -38,11 +39,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} + if user_input is not None: + unique_id = hash_from_url(user_input[CONF_URL]) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() try: info = await _validate_input(user_input) - except CannotConnect: - errors["base"] = "cannot_connect" + except InputValidationError as error: + errors["base"] = error.base except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -54,5 +59,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" +class InputValidationError(exceptions.HomeAssistantError): + """Error to indicate we cannot proceed due to invalid input.""" + + def __init__(self, base: str): + """Initialize with error base.""" + super().__init__() + self.base = base diff --git a/homeassistant/components/nightscout/const.py b/homeassistant/components/nightscout/const.py index f07f37b7b0c..4bb96a94c29 100644 --- a/homeassistant/components/nightscout/const.py +++ b/homeassistant/components/nightscout/const.py @@ -4,6 +4,5 @@ DOMAIN = "nightscout" ATTR_DEVICE = "device" ATTR_DATE = "date" -ATTR_SVG = "svg" ATTR_DELTA = "delta" ATTR_DIRECTION = "direction" diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index fce967b60d5..f4ff14d7b2a 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -1,7 +1,6 @@ """Support for Nightscout sensors.""" from asyncio import TimeoutError as AsyncIOTimeoutError from datetime import timedelta -import hashlib import logging from typing import Callable, List @@ -12,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .const import ATTR_DATE, ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, ATTR_SVG, DOMAIN +from .const import ATTR_DATE, ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN SCAN_INTERVAL = timedelta(minutes=1) @@ -28,16 +27,16 @@ async def async_setup_entry( ) -> None: """Set up the Glucose Sensor.""" api = hass.data[DOMAIN][entry.entry_id] - async_add_entities([NightscoutSensor(api, "Blood Sugar")], True) + async_add_entities([NightscoutSensor(api, "Blood Sugar", entry.unique_id)], True) class NightscoutSensor(Entity): """Implementation of a Nightscout sensor.""" - def __init__(self, api: NightscoutAPI, name): + def __init__(self, api: NightscoutAPI, name, unique_id): """Initialize the Nightscout sensor.""" self.api = api - self._unique_id = hashlib.sha256(api.server_url.encode("utf-8")).hexdigest() + self._unique_id = unique_id self._name = name self._state = None self._attributes = None @@ -75,11 +74,6 @@ class NightscoutSensor(Entity): """Return the icon to use in the frontend, if any.""" return self._icon - @property - def should_poll(self): - """Return the polling state.""" - return True - async def async_update(self): """Fetch the latest data from Nightscout REST API and update the state.""" try: @@ -97,7 +91,6 @@ class NightscoutSensor(Entity): self._attributes = { ATTR_DEVICE: value.device, ATTR_DATE: value.date, - ATTR_SVG: value.sgv, ATTR_DELTA: value.delta, ATTR_DIRECTION: value.direction, } diff --git a/homeassistant/components/nightscout/strings.json b/homeassistant/components/nightscout/strings.json index 08b2bf09361..a6e100ae8f2 100644 --- a/homeassistant/components/nightscout/strings.json +++ b/homeassistant/components/nightscout/strings.json @@ -11,6 +11,9 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ca.json b/homeassistant/components/nightscout/translations/ca.json deleted file mode 100644 index d9b06cbe61e..00000000000 --- a/homeassistant/components/nightscout/translations/ca.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json deleted file mode 100644 index 9c76dd92f9a..00000000000 --- a/homeassistant/components/nightscout/translations/de.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Verbindung nicht m\u00f6glich", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json index c67479819ea..439d42110bc 100644 --- a/homeassistant/components/nightscout/translations/en.json +++ b/homeassistant/components/nightscout/translations/en.json @@ -1,9 +1,13 @@ { "config": { - "error": { - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/es.json b/homeassistant/components/nightscout/translations/es.json deleted file mode 100644 index 05545cdbc48..00000000000 --- a/homeassistant/components/nightscout/translations/es.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "No se pudo conectar", - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json deleted file mode 100644 index 037992b12a3..00000000000 --- a/homeassistant/components/nightscout/translations/fr.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Echec de connexion", - "unknown": "Erreur inattendue" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/it.json b/homeassistant/components/nightscout/translations/it.json deleted file mode 100644 index 2f0790586f3..00000000000 --- a/homeassistant/components/nightscout/translations/it.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Impossibile connettersi", - "unknown": "Errore imprevisto" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json deleted file mode 100644 index 66c2c8822b2..00000000000 --- a/homeassistant/components/nightscout/translations/ko.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "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": { - "url": "URL \uc8fc\uc18c" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/lb.json b/homeassistant/components/nightscout/translations/lb.json deleted file mode 100644 index ca7db64f416..00000000000 --- a/homeassistant/components/nightscout/translations/lb.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "error": { - "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/pl.json b/homeassistant/components/nightscout/translations/pl.json deleted file mode 100644 index bf0d9900695..00000000000 --- a/homeassistant/components/nightscout/translations/pl.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", - "unknown": "Nieoczekiwany b\u0142\u0105d." - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/pt.json b/homeassistant/components/nightscout/translations/pt.json deleted file mode 100644 index 218e7941b42..00000000000 --- a/homeassistant/components/nightscout/translations/pt.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o", - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ru.json b/homeassistant/components/nightscout/translations/ru.json deleted file mode 100644 index a7bd268d56a..00000000000 --- a/homeassistant/components/nightscout/translations/ru.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "url": "URL-\u0430\u0434\u0440\u0435\u0441" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/sl.json b/homeassistant/components/nightscout/translations/sl.json deleted file mode 100644 index 33b65a99f8a..00000000000 --- a/homeassistant/components/nightscout/translations/sl.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Povezava ni uspela", - "unknown": "Nepri\u010dakovana napaka" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json deleted file mode 100644 index cf83adfc35a..00000000000 --- a/homeassistant/components/nightscout/translations/zh-Hant.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "url": "\u7db2\u5740" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/nightscout/utils.py b/homeassistant/components/nightscout/utils.py new file mode 100644 index 00000000000..4d262ee6439 --- /dev/null +++ b/homeassistant/components/nightscout/utils.py @@ -0,0 +1,7 @@ +"""Nightscout util functions.""" +import hashlib + + +def hash_from_url(url: str): + """Hash url to create a unique ID.""" + return hashlib.sha256(url.encode("utf-8")).hexdigest() diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index 9db86759658..99eae99160a 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -3,9 +3,11 @@ from aiohttp import ClientConnectionError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.nightscout.const import DOMAIN +from homeassistant.components.nightscout.utils import hash_from_url from homeassistant.const import CONF_URL from tests.async_mock import patch +from tests.common import MockConfigEntry from tests.components.nightscout import GLUCOSE_READINGS, SERVER_STATUS CONFIG = {CONF_URL: "https://some.url:1234"} @@ -20,13 +22,7 @@ async def test_form(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - with patch( - "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", - return_value=GLUCOSE_READINGS, - ), patch( - "homeassistant.components.nightscout.NightscoutAPI.get_server_status", - return_value=SERVER_STATUS, - ), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + with _patch_glucose_readings(), _patch_server_status(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, ) @@ -53,12 +49,12 @@ async def test_user_form_cannot_connect(hass): result["flow_id"], {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} async def test_user_form_unexpected_exception(hass): - """Test we handle cannot connect error.""" + """Test we handle unexpected exception.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -71,10 +67,23 @@ async def test_user_form_unexpected_exception(hass): result["flow_id"], {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "unknown"} +async def test_user_form_duplicate(hass): + """Test duplicate entries.""" + with _patch_glucose_readings(), _patch_server_status(): + unique_id = hash_from_url(CONFIG[CONF_URL]) + entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id) + await hass.config_entries.async_add(entry) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + def _patch_async_setup(): return patch("homeassistant.components.nightscout.async_setup", return_value=True) @@ -83,3 +92,17 @@ def _patch_async_setup_entry(): return patch( "homeassistant.components.nightscout.async_setup_entry", return_value=True, ) + + +def _patch_glucose_readings(): + return patch( + "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", + return_value=GLUCOSE_READINGS, + ) + + +def _patch_server_status(): + return patch( + "homeassistant.components.nightscout.NightscoutAPI.get_server_status", + return_value=SERVER_STATUS, + ) diff --git a/tests/components/nightscout/test_sensor.py b/tests/components/nightscout/test_sensor.py index c2fcfe543e7..3df98a2595a 100644 --- a/tests/components/nightscout/test_sensor.py +++ b/tests/components/nightscout/test_sensor.py @@ -5,7 +5,6 @@ from homeassistant.components.nightscout.const import ( ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, - ATTR_SVG, ) from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE @@ -56,5 +55,4 @@ async def test_sensor_attributes(hass): assert attr[ATTR_DELTA] == reading.delta # pylint: disable=maybe-no-member assert attr[ATTR_DEVICE] == reading.device # pylint: disable=maybe-no-member assert attr[ATTR_DIRECTION] == reading.direction # pylint: disable=maybe-no-member - assert attr[ATTR_SVG] == reading.sgv # pylint: disable=maybe-no-member assert attr[ATTR_ICON] == "mdi:arrow-bottom-right" From ca5e752514f05c9acc3c3a8f144a6a36d093fd3d Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 13 Aug 2020 07:52:30 -0400 Subject: [PATCH 149/862] Add switch support to the Flo integration (#38268) * Add switch domain to Flo integration * lint * updates post rebase * fix after rebase * remove device state attrs * oops * stale name and docstring --- homeassistant/components/flo/__init__.py | 2 +- homeassistant/components/flo/device.py | 10 + homeassistant/components/flo/switch.py | 59 +++++ tests/components/flo/conftest.py | 16 ++ tests/components/flo/test_device.py | 2 + tests/components/flo/test_switch.py | 31 +++ .../flo/device_info_response_closed.json | 238 ++++++++++++++++++ 7 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/flo/switch.py create mode 100644 tests/components/flo/test_switch.py create mode 100644 tests/fixtures/flo/device_info_response_closed.json diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index b02a848de7c..6bbadf0e89d 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -19,7 +19,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["binary_sensor", "sensor"] +PLATFORMS = ["binary_sensor", "sensor", "switch"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index abba045693d..179a293ba20 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -162,6 +162,16 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): or self.pending_warning_alerts_count ) + @property + def last_known_valve_state(self) -> str: + """Return the last known valve state for the device.""" + return self._device_information["valve"]["lastKnown"] + + @property + def target_valve_state(self) -> str: + """Return the target valve state for the device.""" + return self._device_information["valve"]["target"] + async def _update_device(self, *_) -> None: """Update the device information from the API.""" self._device_information = await self.api_client.device.get_info( diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py new file mode 100644 index 00000000000..cabf8135ad9 --- /dev/null +++ b/homeassistant/components/flo/switch.py @@ -0,0 +1,59 @@ +"""Switch representing the shutoff valve for the Flo by Moen integration.""" + +from typing import List + +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import callback + +from .const import DOMAIN as FLO_DOMAIN +from .device import FloDeviceDataUpdateCoordinator +from .entity import FloEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Flo switches from config entry.""" + devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN]["devices"] + async_add_entities([FloSwitch(device) for device in devices]) + + +class FloSwitch(FloEntity, SwitchEntity): + """Switch class for the Flo by Moen valve.""" + + def __init__(self, device: FloDeviceDataUpdateCoordinator): + """Initialize the Flo switch.""" + super().__init__("shutoff_valve", "Shutoff Valve", device) + self._state = self._device.last_known_valve_state == "open" + + @property + def is_on(self) -> bool: + """Return True if the valve is open.""" + return self._state + + @property + def icon(self): + """Return the icon to use for the valve.""" + if self.is_on: + return "mdi:valve-open" + return "mdi:valve-closed" + + async def async_turn_on(self, **kwargs) -> None: + """Open the valve.""" + await self._device.api_client.device.open_valve(self._device.id) + self._state = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Close the valve.""" + await self._device.api_client.device.close_valve(self._device.id) + self._state = False + self.async_write_ha_state() + + @callback + def async_update_state(self) -> None: + """Retrieve the latest valve state and update the state machine.""" + self._state = self._device.last_known_valve_state == "open" + self.async_write_ha_state() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove(self._device.async_add_listener(self.async_update_state)) diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py index 69167b58a02..982bdbdec0d 100644 --- a/tests/components/flo/conftest.py +++ b/tests/components/flo/conftest.py @@ -79,3 +79,19 @@ def aioclient_mock_fixture(aioclient_mock): status=200, headers={"Content-Type": "application/json"}, ) + # Mocks the valve open call for flo. + aioclient_mock.post( + "https://api-gw.meetflo.com/api/v2/devices/98765", + text=load_fixture("flo/device_info_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + json={"valve": {"target": "open"}}, + ) + # Mocks the valve close call for flo. + aioclient_mock.post( + "https://api-gw.meetflo.com/api/v2/devices/98765", + text=load_fixture("flo/device_info_response_closed.json"), + status=200, + headers={"Content-Type": "application/json"}, + json={"valve": {"target": "closed"}}, + ) diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py index 5f5a035ecbd..db5c8cd5c9e 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -45,6 +45,8 @@ async def test_device(hass, config_entry, aioclient_mock_fixture, aioclient_mock assert device.pending_critical_alerts_count == 0 assert device.pending_warning_alerts_count == 2 assert device.has_alerts is True + assert device.last_known_valve_state == "open" + assert device.target_valve_state == "open" call_count = aioclient_mock.call_count diff --git a/tests/components/flo/test_switch.py b/tests/components/flo/test_switch.py new file mode 100644 index 00000000000..f821e5f57d9 --- /dev/null +++ b/tests/components/flo/test_switch.py @@ -0,0 +1,31 @@ +"""Tests for the switch domain for Flo by Moen.""" +from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN +from homeassistant.components.switch import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from .common import TEST_PASSWORD, TEST_USER_ID + + +async def test_valve_switches(hass, config_entry, aioclient_mock_fixture): + """Test Flo by Moen valve switches.""" + config_entry.add_to_hass(hass) + assert await async_setup_component( + hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} + ) + await hass.async_block_till_done() + + assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + + entity_id = "switch.shutoff_valve" + assert hass.states.get(entity_id).state == STATE_ON + + await hass.services.async_call( + DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON diff --git a/tests/fixtures/flo/device_info_response_closed.json b/tests/fixtures/flo/device_info_response_closed.json new file mode 100644 index 00000000000..28ae3d8154c --- /dev/null +++ b/tests/fixtures/flo/device_info_response_closed.json @@ -0,0 +1,238 @@ +{ + "isConnected": true, + "fwVersion": "6.1.1", + "lastHeardFromTime": "2020-07-24T12:45:00Z", + "fwProperties": { + "alarm_away_high_flow_rate_shut_off_enabled": true, + "alarm_away_high_water_use_shut_off_enabled": true, + "alarm_away_long_flow_event_shut_off_enabled": true, + "alarm_away_v2_shut_off_enabled": true, + "alarm_home_high_flow_rate_shut_off_deferment": 300, + "alarm_home_high_flow_rate_shut_off_enabled": true, + "alarm_home_high_water_use_shut_off_deferment": 300, + "alarm_home_high_water_use_shut_off_enabled": true, + "alarm_home_long_flow_event_shut_off_deferment": 300, + "alarm_home_long_flow_event_shut_off_enabled": true, + "alarm_shut_off_enabled": true, + "alarm_shutoff_id": "", + "alarm_shutoff_time_epoch_sec": -1, + "alarm_snooze_enabled": true, + "alarm_suppress_duplicate_duration": 300, + "alarm_suppress_until_event_end": false, + "data_flosense_force_retrain": 1, + "data_flosense_min_flodetect_sec": 0, + "data_flosense_min_irr_sec": 180, + "data_flosense_status_interval": 1200, + "data_flosense_verbosity": 1, + "device_data_free_mb": 1465, + "device_installed": true, + "device_mem_available_kb": 339456, + "device_rootfs_free_kb": 711504, + "device_uptime_sec": 867190, + "feature_mode": "default", + "flodetect_post_enabled": true, + "flodetect_post_frequency": 0, + "flodetect_storage_days": 60, + "flosense_action": "", + "flosense_deployment_result": "success", + "flosense_link": "", + "flosense_shut_off_enabled": true, + "flosense_shut_off_level": 3, + "flosense_state": "active", + "flosense_version_app": "2.5.3", + "flosense_version_model": "2.5.0", + "fw_ver": "6.1.1", + "fw_ver_a": "6.1.1", + "fw_ver_b": "6.0.3", + "heartbeat_frequency": 1800, + "ht_attempt_interval": 60000, + "ht_check_window_max_pressure_decay_limit": 0.1, + "ht_check_window_width": 30000, + "ht_controller": "ultima", + "ht_max_open_closed_pressure_decay_pct_limit": 2, + "ht_max_pressure_growth_limit": 3, + "ht_max_pressure_growth_pct_limit": 3, + "ht_max_valve_closures_per_24h": 0, + "ht_min_computable_point_limit": 3, + "ht_min_pressure_limit": 10, + "ht_min_r_squared_limit": 0.9, + "ht_min_slope_limit": -0.6, + "ht_phase_1_max_pressure_decay_limit": 6, + "ht_phase_1_max_pressure_decay_pct_limit": 10, + "ht_phase_1_time_index": 12000, + "ht_phase_2_max_pressure_decay_limit": 6, + "ht_phase_2_max_pressure_decay_pct_limit": 10, + "ht_phase_2_time_index": 30000, + "ht_phase_3_max_pressure_decay_limit": 3, + "ht_phase_3_max_pressure_decay_pct_limit": 5, + "ht_phase_3_time_index": 240000, + "ht_phase_4_max_pressure_decay_limit": 1.5, + "ht_phase_4_max_pressure_decay_pct_limit": 5, + "ht_phase_4_time_index": 480000, + "ht_pre_delay": 0, + "ht_recent_flow_event_cool_down": 1000, + "ht_retry_on_fail_interval": 900000, + "ht_scheduler": "flosense", + "ht_scheduler_end": "08:00", + "ht_scheduler_start": "06:00", + "ht_scheduler_ultima_allotted_time_1": "06:00", + "ht_scheduler_ultima_allotted_time_2": "07:00", + "ht_scheduler_ultima_allotted_time_3": "", + "ht_times_per_day": 1, + "log_bytes_sent": 0, + "log_enabled": true, + "log_frequency": 3600, + "log_send": false, + "mender_check": false, + "mender_host": "https://mender.flotech.co", + "mender_parts_link": "", + "mender_ping_delay": 300, + "mender_signature": "20200610", + "motor_delay_close": 175, + "motor_delay_open": 0, + "motor_retry_count": 2, + "motor_timeout": 5000, + "mqtt_host": "mqtt.flosecurecloud.com", + "mqtt_port": 8884, + "pes_away_max_duration": 1505, + "pes_away_max_pressure": 150, + "pes_away_max_temperature": 226, + "pes_away_max_volume": 91.8913240498193, + "pes_away_min_pressure": 20, + "pes_away_min_pressure_duration": 5, + "pes_away_min_temperature": 36, + "pes_away_min_temperature_duration": 10, + "pes_away_v1_high_flow_rate": 7.825131772346, + "pes_away_v1_high_flow_rate_duration": 5, + "pes_away_v2_high_flow_rate": 0.5, + "pes_away_v2_high_flow_rate_duration": 5, + "pes_home_high_flow_rate": 1000, + "pes_home_high_flow_rate_duration": 20, + "pes_home_max_duration": 7431, + "pes_home_max_pressure": 150, + "pes_home_max_temperature": 226, + "pes_home_max_volume": 185.56459045410156, + "pes_home_min_pressure": 20, + "pes_home_min_pressure_duration": 5, + "pes_home_min_temperature": 36, + "pes_home_min_temperature_duration": 10, + "pes_moderately_high_pressure": 80, + "pes_moderately_high_pressure_count": 43200, + "pes_moderately_high_pressure_delay": 300, + "pes_moderately_high_pressure_period": 10, + "player_action": "disabled", + "player_flow": 0, + "player_min_pressure": 40, + "player_pressure": 60, + "player_temperature": 50, + "power_downtime_last_24h": 91, + "power_downtime_last_7days": 91, + "power_downtime_last_reboot": 91, + "pt_state": "ok", + "reboot_count": 26, + "reboot_count_7days": 1, + "reboot_reason": "power_cycle", + "s3_bucket_host": "api-bulk.meetflo.com", + "serial_number": "111111111111", + "system_mode": 2, + "tag": "", + "telemetry_batched_enabled": true, + "telemetry_batched_hf_enabled": true, + "telemetry_batched_hf_interval": 10800, + "telemetry_batched_hf_poll_rate": 100, + "telemetry_batched_interval": 300, + "telemetry_batched_pending_storage": 30, + "telemetry_batched_sent_storage": 30, + "telemetry_flow_rate": 0, + "telemetry_pressure": 42.4, + "telemetry_realtime_change_gpm": 0, + "telemetry_realtime_change_psi": 0, + "telemetry_realtime_enabled": true, + "telemetry_realtime_interval": 1, + "telemetry_realtime_packet_uptime": 0, + "telemetry_realtime_session_last_epoch": 1595555701518, + "telemetry_realtime_sessions_7days": 25, + "telemetry_realtime_storage": 7, + "telemetry_realtime_timeout": 300, + "telemetry_temperature": 68, + "valve_actuation_count": 906, + "valve_actuation_timeout_count": 0, + "valve_state": 1, + "vpn_enabled": false, + "vpn_ip": "", + "water_event_enabled": false, + "water_event_min_duration": 2, + "water_event_min_gallons": 0.1, + "wifi_bytes_received": 24164, + "wifi_bytes_sent": 18319, + "wifi_disconnections": 76, + "wifi_rssi": -50, + "wifi_sta_enc": "psk2", + "wifi_sta_ip": "192.168.1.1", + "wifi_sta_ssid": "SOMESSID", + "zit_auto_count": 2363, + "zit_manual_count": 0 + }, + "id": "98765", + "macAddress": "111111111111", + "nickname": "Smart Water Shutoff", + "isPaired": true, + "deviceModel": "flo_device_075_v2", + "deviceType": "flo_device_v2", + "irrigationType": "sprinklers", + "systemMode": { + "isLocked": false, + "shouldInherit": true, + "lastKnown": "home", + "target": "home" + }, + "valve": { "target": "closed", "lastKnown": "closed" }, + "installStatus": { + "isInstalled": true, + "installDate": "2019-05-04T13:50:04.758Z" + }, + "learning": { "outOfLearningDate": "2019-05-10T21:45:48.916Z" }, + "notifications": { + "pending": { + "infoCount": 0, + "warningCount": 2, + "criticalCount": 0, + "alarmCount": [ + { "id": 30, "severity": "warning", "count": 1 }, + { "id": 31, "severity": "warning", "count": 1 } + ], + "info": { "count": 0, "devices": { "count": 0, "absolute": 0 } }, + "warning": { "count": 2, "devices": { "count": 1, "absolute": 1 } }, + "critical": { "count": 0, "devices": { "count": 0, "absolute": 0 } } + } + }, + "hardwareThresholds": { + "gpm": { "okMin": 0, "okMax": 29, "minValue": 0, "maxValue": 35 }, + "psi": { "okMin": 30, "okMax": 80, "minValue": 0, "maxValue": 100 }, + "lpm": { "okMin": 0, "okMax": 110, "minValue": 0, "maxValue": 130 }, + "kPa": { "okMin": 210, "okMax": 550, "minValue": 0, "maxValue": 700 }, + "tempF": { "okMin": 50, "okMax": 80, "minValue": 0, "maxValue": 100 }, + "tempC": { "okMin": 10, "okMax": 30, "minValue": 0, "maxValue": 40 } + }, + "serialNumber": "111111111111", + "connectivity": { "rssi": -47, "ssid": "SOMESSID" }, + "telemetry": { + "current": { + "gpm": 0, + "psi": 54.20000076293945, + "tempF": 70, + "updated": "2020-07-24T12:20:58Z" + } + }, + "healthTest": { + "config": { + "enabled": true, + "timesPerDay": 1, + "start": "02:00", + "end": "04:00" + } + }, + "shutoff": { "scheduledAt": "1970-01-01T00:00:00.000Z" }, + "actionRules": [], + "location": { "id": "12345abcde" } +} From 18833d342e7763d42365a590187a9a1711488e99 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 13 Aug 2020 10:16:28 -0400 Subject: [PATCH 150/862] Fix typo in media_player docstring (#38843) --- homeassistant/components/media_player/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index ab70a339d24..372b34eae45 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -1,4 +1,4 @@ -"""Proides the constants needed for component.""" +"""Provides the constants needed for component.""" ATTR_APP_ID = "app_id" ATTR_APP_NAME = "app_name" From ee64aafc3932ea0a7a76a33d1827db0c78fc0ed3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Aug 2020 11:47:18 -0500 Subject: [PATCH 151/862] Fix iqvia test patching the wrong integration (#38847) --- tests/components/iqvia/test_config_flow.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 6b6872d5f67..209f88cf895 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -47,9 +47,7 @@ async def test_step_user(hass): """Test that the user step works (without MFA).""" conf = {CONF_ZIP_CODE: "12345"} - with patch( - "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ): + with patch("homeassistant.components.iqvia.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf ) From 72472cd11fa52533978250eebc94e2d91b1eda27 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 14 Aug 2020 03:39:17 +0200 Subject: [PATCH 152/862] Remove superfluous netatmo icons (#38859) --- homeassistant/components/netatmo/sensor.py | 34 +++++----------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 2c55e76df79..c424672dae0 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -47,39 +47,19 @@ SUPPORTED_PUBLIC_SENSOR_TYPES = [ ] SENSOR_TYPES = { - "temperature": [ - "Temperature", - TEMP_CELSIUS, - "mdi:thermometer", - DEVICE_CLASS_TEMPERATURE, - ], + "temperature": ["Temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2", None], - "pressure": ["Pressure", PRESSURE_MBAR, "mdi:gauge", DEVICE_CLASS_PRESSURE], + "pressure": ["Pressure", PRESSURE_MBAR, None, DEVICE_CLASS_PRESSURE], "noise": ["Noise", "dB", "mdi:volume-high", None], - "humidity": [ - "Humidity", - UNIT_PERCENTAGE, - "mdi:water-percent", - DEVICE_CLASS_HUMIDITY, - ], + "humidity": ["Humidity", UNIT_PERCENTAGE, None, DEVICE_CLASS_HUMIDITY], "rain": ["Rain", "mm", "mdi:weather-rainy", None], "sum_rain_1": ["Rain last hour", "mm", "mdi:weather-rainy", None], "sum_rain_24": ["Rain last 24h", "mm", "mdi:weather-rainy", None], "battery_vp": ["Battery", "", "mdi:battery", None], "battery_lvl": ["Battery Level", "", "mdi:battery", None], "battery_percent": ["Battery Percent", UNIT_PERCENTAGE, None, DEVICE_CLASS_BATTERY], - "min_temp": [ - "Min Temp.", - TEMP_CELSIUS, - "mdi:thermometer", - DEVICE_CLASS_TEMPERATURE, - ], - "max_temp": [ - "Max Temp.", - TEMP_CELSIUS, - "mdi:thermometer", - DEVICE_CLASS_TEMPERATURE, - ], + "min_temp": ["Min Temp.", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + "max_temp": ["Max Temp.", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], "windangle": ["Angle", None, "mdi:compass-outline", None], "windangle_value": ["Angle Value", DEGREE, "mdi:compass-outline", None], "windstrength": [ @@ -98,9 +78,9 @@ SENSOR_TYPES = { ], "reachable": ["Reachability", None, "mdi:signal", None], "rf_status": ["Radio", None, "mdi:signal", None], - "rf_status_lvl": ["Radio Level", "", "mdi:signal", DEVICE_CLASS_SIGNAL_STRENGTH], + "rf_status_lvl": ["Radio Level", "", None, DEVICE_CLASS_SIGNAL_STRENGTH], "wifi_status": ["Wifi", None, "mdi:wifi", None], - "wifi_status_lvl": ["Wifi Level", "dBm", "mdi:wifi", DEVICE_CLASS_SIGNAL_STRENGTH], + "wifi_status_lvl": ["Wifi Level", "dBm", None, DEVICE_CLASS_SIGNAL_STRENGTH], "health_idx": ["Health", None, "mdi:cloud", None], } From fceba0bb8834e8a1308eee8abcbc1210d2871be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Fri, 14 Aug 2020 03:48:26 +0200 Subject: [PATCH 153/862] Bump pysyncthru to 0.7.0 (#38832) * Bump pysyncthru version to 0.7.0 This change includes a heavier refactoring, using a more reliable source for the device status and parsing display strings only for additional details * Fix test flow to ensure a status is set --- homeassistant/components/syncthru/manifest.json | 2 +- homeassistant/components/syncthru/sensor.py | 7 ++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/syncthru/test_config_flow.py | 1 + 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index bf62738a02e..f70afa5a695 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,7 +3,7 @@ "name": "Samsung SyncThru Printer", "documentation": "https://www.home-assistant.io/integrations/syncthru", "config_flow": true, - "requirements": ["pysyncthru==0.5.0", "url-normalize==1.4.1"], + "requirements": ["pysyncthru==0.7.0", "url-normalize==1.4.1"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:Printer:1", diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 6f7f248a924..c9aa6d8de9c 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -2,7 +2,7 @@ import logging -from pysyncthru import SyncThru +from pysyncthru import SYNCTHRU_STATE_HUMAN, SyncThru import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -101,7 +101,7 @@ class SyncThruSensor(Entity): def __init__(self, syncthru, name): """Initialize the sensor.""" - self.syncthru = syncthru + self.syncthru: SyncThru = syncthru self._attributes = {} self._state = None self._name = name @@ -164,7 +164,8 @@ class SyncThruMainSensor(SyncThruSensor): self.syncthru.url, ) self._active = False - self._state = self.syncthru.device_status() + self._state = SYNCTHRU_STATE_HUMAN[self.syncthru.device_status()] + self._attributes = {"display_text": self.syncthru.device_status_details()} class SyncThruTonerSensor(SyncThruSensor): diff --git a/requirements_all.txt b/requirements_all.txt index 66971ebbe57..1498e37fd09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1668,7 +1668,7 @@ pysuez==0.1.17 pysupla==0.0.3 # homeassistant.components.syncthru -pysyncthru==0.5.0 +pysyncthru==0.7.0 # homeassistant.components.tankerkoenig pytankerkoenig==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 393a99e5e1a..18878a13833 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ pyspcwebgw==0.4.0 pysqueezebox==0.2.4 # homeassistant.components.syncthru -pysyncthru==0.5.0 +pysyncthru==0.7.0 # homeassistant.components.ecobee python-ecobee-api==0.2.7 diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index aac1923caab..8eb72ee264a 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -24,6 +24,7 @@ def mock_connection(aioclient_mock): text=""" { \tstatus: { +\thrDeviceStatus: 2, \tstatus1: " Sleeping... " \t}, \tidentity: { From f26a49eca513cb016430f886680e816417f0e3cb Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 14 Aug 2020 03:56:06 +0200 Subject: [PATCH 154/862] Fix Freebox unsub dispatcher (#38842) --- homeassistant/components/freebox/router.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 3ebc3d754c3..daa57a89c47 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -52,6 +52,7 @@ class FreeboxRouter: self.sensors_connection: Dict[str, float] = {} self.call_list: List[Dict[str, Any]] = [] + self._unsub_dispatcher = None self.listeners = [] async def setup(self) -> None: @@ -72,7 +73,9 @@ class FreeboxRouter: # Devices & sensors await self.update_all() - async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) + self._unsub_dispatcher = async_track_time_interval( + self.hass, self.update_all, SCAN_INTERVAL + ) async def update_all(self, now: Optional[datetime] = None) -> None: """Update all Freebox platforms.""" @@ -147,6 +150,7 @@ class FreeboxRouter: """Close the connection.""" if self._api is not None: await self._api.close() + self._unsub_dispatcher() self._api = None @property From 2f955ccfd7c45f735e4bf552d4587f9a6561615d Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 13 Aug 2020 19:00:55 -0700 Subject: [PATCH 155/862] Bump pywemo to 0.4.46 (#38845) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 7bb3371c153..6200c3b46ed 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.4.45"], + "requirements": ["pywemo==0.4.46"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 1498e37fd09..f277f9741a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1846,7 +1846,7 @@ pyvolumio==0.1.1 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.4.45 +pywemo==0.4.46 # homeassistant.components.xeoma pyxeoma==1.4.1 From 49478298ccfdc2dbaed74a19775cb790e49cf4b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Aug 2020 22:00:39 -0500 Subject: [PATCH 156/862] Ensure service browser does not collapse on bad dns names (#38851) If a device on the network presented a bad name, zeroconf would throw zeroconf.BadTypeInNameException and the service browser would die off. We now trap the exception and continue. --- homeassistant/components/zeroconf/__init__.py | 8 ++++++- tests/components/zeroconf/test_init.py | 22 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 5bbc87f3da8..1eeef3917d7 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol from zeroconf import ( DNSPointer, DNSRecord, + Error as ZeroconfError, InterfaceChoice, IPVersion, NonUniqueNameException, @@ -212,7 +213,12 @@ def setup(hass, config): if state_change != ServiceStateChange.Added: return - service_info = zeroconf.get_service_info(service_type, name) + try: + service_info = zeroconf.get_service_info(service_type, name) + except ZeroconfError: + _LOGGER.exception("Failed to get info for device %s", name) + return + if not service_info: # Prevent the browser thread from collapsing as # service_info can be None diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 2ced2fac8ea..32891a04262 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,5 +1,11 @@ """Test Zeroconf component setup process.""" -from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange +from zeroconf import ( + BadTypeInNameException, + InterfaceChoice, + IPVersion, + ServiceInfo, + ServiceStateChange, +) from homeassistant.components import zeroconf from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 @@ -167,6 +173,20 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf): assert mock_zeroconf.called_with() +async def test_service_with_invalid_name(hass, mock_zeroconf, caplog): + """Test we do not crash on service with an invalid name.""" + with patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = BadTypeInNameException + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert "Failed to get info for device name" in caplog.text + + async def test_homekit_match_partial_space(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( From 23510e68399e7e092f577e8cf84381de0ca78fad Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 14 Aug 2020 11:15:45 +0200 Subject: [PATCH 157/862] Disable env_canada pylint import error (#38868) * Disable env_canada pylint import error * Disable pylint in camera and sensor too --- homeassistant/components/environment_canada/camera.py | 2 +- homeassistant/components/environment_canada/sensor.py | 2 +- homeassistant/components/environment_canada/weather.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index c0565988c65..a30ffed55df 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -2,7 +2,7 @@ import datetime import logging -from env_canada import ECRadar +from env_canada import ECRadar # pylint: disable=import-error import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 601a7f2ba36..afd75956a9f 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import logging import re -from env_canada import ECData +from env_canada import ECData # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 78ede4dbc5e..ca3c1f13c14 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -3,7 +3,7 @@ import datetime import logging import re -from env_canada import ECData +from env_canada import ECData # pylint: disable=import-error import voluptuous as vol from homeassistant.components.weather import ( From 2bc533d0cb0cc39b5c7a76870810b384e8ddab73 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 14 Aug 2020 13:27:11 +0200 Subject: [PATCH 158/862] Fix logger name (#38866) --- homeassistant/components/input_boolean/__init__.py | 2 +- homeassistant/components/tag/__init__.py | 2 +- homeassistant/components/zone/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 88fe94eac48..f123d6d3297 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -95,7 +95,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection = InputBooleanStorageCollection( Store(hass, STORAGE_VERSION, STORAGE_KEY), - logging.getLogger(f"{__name__}_storage_collection"), + logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) collection.attach_entity_component_collection( diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 968b74e226d..01b283bb778 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -95,7 +95,7 @@ async def async_setup(hass: HomeAssistant, config: dict): id_manager = TagIDManager() hass.data[DOMAIN][TAGS] = storage_collection = TagStorageCollection( Store(hass, STORAGE_VERSION, STORAGE_KEY), - logging.getLogger(f"{__name__}_storage_collection"), + logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) await storage_collection.async_load() diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 2a7c3f01a27..5f65f1e9596 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -188,7 +188,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: storage_collection = ZoneStorageCollection( storage.Store(hass, STORAGE_VERSION, STORAGE_KEY), - logging.getLogger(f"{__name__}_storage_collection"), + logging.getLogger(f"{__name__}.storage_collection"), id_manager, ) collection.attach_entity_component_collection( From f4f614a0bc0ff18c36c2564c55cbb569e009da3e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 14 Aug 2020 14:03:39 +0200 Subject: [PATCH 159/862] Add sympathy review check box to PR template (#38867) Co-authored-by: Franck Nijhof --- .github/PULL_REQUEST_TEMPLATE.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1ada6d3af86..455daf47e1e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,7 +13,7 @@ ## Proposed change - @@ -98,6 +98,29 @@ The integration reached or maintains the following [Integration Quality Scale][q - [ ] 🥇 Gold - [ ] 🏆 Platinum + + +To help with the load of incoming pull requests: + +- [ ] I have reviewed two other [open pull requests][prs] in this repository. + +[prs]: https://github.com/home-assistant/core/pulls?page=5&q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-asc+-review%3Aapproved + async_add_executor_job * Review: const * Review: start logging messages with capital letter * Review : UTC isoformated time --> fix "Invalid date"" * Fix hail forecast condition * Review: _show_setup_form is a callback * Fix update option * Review: no icon for next_rain * Review: inline cities form * Review: store places as an instance attribute * UNDO_UPDATE_LISTENER() --- .../components/meteo_france/__init__.py | 20 +++++-- .../components/meteo_france/config_flow.py | 58 ++++++++++--------- .../components/meteo_france/const.py | 36 +++++++----- .../components/meteo_france/sensor.py | 20 ++++--- .../components/meteo_france/weather.py | 5 +- 5 files changed, 82 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 469c66ad79f..276aac188d9 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -20,6 +20,7 @@ from .const import ( COORDINATOR_RAIN, DOMAIN, PLATFORMS, + UNDO_UPDATE_LISTENER, ) _LOGGER = logging.getLogger(__name__) @@ -77,15 +78,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool async def _async_update_data_forecast_forecast(): """Fetch data from API endpoint.""" - return await hass.async_add_job(client.get_forecast, latitude, longitude) + return await hass.async_add_executor_job( + client.get_forecast, latitude, longitude + ) async def _async_update_data_rain(): """Fetch data from API endpoint.""" - return await hass.async_add_job(client.get_rain, latitude, longitude) + return await hass.async_add_executor_job(client.get_rain, latitude, longitude) async def _async_update_data_alert(): """Fetch data from API endpoint.""" - return await hass.async_add_job( + return await hass.async_add_executor_job( client.get_warning_current_phenomenoms, department, 0, True ) @@ -156,10 +159,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool entry.title, ) + undo_listener = entry.add_update_listener(_async_update_listener) + hass.data[DOMAIN][entry.entry_id] = { COORDINATOR_FORECAST: coordinator_forecast, COORDINATOR_RAIN: coordinator_rain, COORDINATOR_ALERT: coordinator_alert, + UNDO_UPDATE_LISTENER: undo_listener, } for platform in PLATFORMS: @@ -192,8 +198,14 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): ) ) if unload_ok: + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) - if len(hass.data[DOMAIN]) == 0: + if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) return unload_ok + + +async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index 73b1ea41089..0854c280c16 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -21,13 +21,18 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self): + """Init MeteoFranceFlowHandler.""" + self.places = [] + @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return MeteoFranceOptionsFlowHandler(config_entry) - async def _show_setup_form(self, user_input=None, errors=None): + @callback + def _show_setup_form(self, user_input=None, errors=None): """Show the setup form to the user.""" if user_input is None: @@ -46,7 +51,7 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is None: - return await self._show_setup_form(user_input, errors) + return self._show_setup_form(user_input, errors) city = user_input[CONF_CITY] # Might be a city name or a postal code latitude = user_input.get(CONF_LATITUDE) @@ -54,13 +59,15 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not latitude: client = MeteoFranceClient() - places = await self.hass.async_add_executor_job(client.search_places, city) - _LOGGER.debug("places search result: %s", places) - if not places: + self.places = await self.hass.async_add_executor_job( + client.search_places, city + ) + _LOGGER.debug("Places search result: %s", self.places) + if not self.places: errors[CONF_CITY] = "empty" - return await self._show_setup_form(user_input, errors) + return self._show_setup_form(user_input, errors) - return await self.async_step_cities(places=places) + return await self.async_step_cities() # Check if already configured await self.async_set_unique_id(f"{latitude}, {longitude}") @@ -74,19 +81,27 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Import a config entry.""" return await self.async_step_user(user_input) - async def async_step_cities(self, user_input=None, places=None): + async def async_step_cities(self, user_input=None): """Step where the user choose the city from the API search results.""" - if places and len(places) > 1 and self.source != SOURCE_IMPORT: - places_for_form = {} - for place in places: - places_for_form[_build_place_key(place)] = f"{place}" + if not user_input: + if len(self.places) > 1 and self.source != SOURCE_IMPORT: + places_for_form = {} + for place in self.places: + places_for_form[_build_place_key(place)] = f"{place}" - return await self._show_cities_form(places_for_form) - # for import and only 1 city in the search result - if places and not user_input: - user_input = {CONF_CITY: _build_place_key(places[0])} + return self.async_show_form( + step_id="cities", + data_schema=vol.Schema( + { + vol.Required(CONF_CITY): vol.All( + vol.Coerce(str), vol.In(places_for_form) + ) + } + ), + ) + user_input = {CONF_CITY: _build_place_key(self.places[0])} - city_infos = user_input.get(CONF_CITY).split(";") + city_infos = user_input[CONF_CITY].split(";") return await self.async_step_user( { CONF_CITY: city_infos[0], @@ -95,15 +110,6 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def _show_cities_form(self, cities): - """Show the form to choose the city.""" - return self.async_show_form( - step_id="cities", - data_schema=vol.Schema( - {vol.Required(CONF_CITY): vol.All(vol.Coerce(str), vol.In(cities))} - ), - ) - class MeteoFranceOptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow.""" diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index d1decb54078..8e6c625e331 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -1,6 +1,9 @@ """Meteo-France component constants.""" from homeassistant.const import ( + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, @@ -12,6 +15,7 @@ PLATFORMS = ["sensor", "weather"] COORDINATOR_FORECAST = "coordinator_forecast" COORDINATOR_RAIN = "coordinator_rain" COORDINATOR_ALERT = "coordinator_alert" +UNDO_UPDATE_LISTENER = "undo_update_listener" ATTRIBUTION = "Data provided by Météo-France" CONF_CITY = "city" @@ -24,7 +28,7 @@ ATTR_NEXT_RAIN_1_HOUR_FORECAST = "1_hour_forecast" ENTITY_NAME = "name" ENTITY_UNIT = "unit" ENTITY_ICON = "icon" -ENTITY_CLASS = "device_class" +ENTITY_DEVICE_CLASS = "device_class" ENTITY_ENABLE = "enable" ENTITY_API_DATA_PATH = "data_path" @@ -32,8 +36,8 @@ SENSOR_TYPES = { "pressure": { ENTITY_NAME: "Pressure", ENTITY_UNIT: PRESSURE_HPA, - ENTITY_ICON: "mdi:gauge", - ENTITY_CLASS: "pressure", + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, ENTITY_ENABLE: False, ENTITY_API_DATA_PATH: "current_forecast:sea_level", }, @@ -41,7 +45,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Rain chance", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:weather-rainy", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "probability_forecast:rain:3h", }, @@ -49,7 +53,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Snow chance", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:weather-snowy", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "probability_forecast:snow:3h", }, @@ -57,7 +61,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Freeze chance", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:snowflake", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "probability_forecast:freezing", }, @@ -65,23 +69,23 @@ SENSOR_TYPES = { ENTITY_NAME: "Wind speed", ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR, ENTITY_ICON: "mdi:weather-windy", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: False, ENTITY_API_DATA_PATH: "current_forecast:wind:speed", }, "next_rain": { ENTITY_NAME: "Next rain", ENTITY_UNIT: None, - ENTITY_ICON: "mdi:weather-pouring", - ENTITY_CLASS: "timestamp", + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: None, }, "temperature": { ENTITY_NAME: "Temperature", ENTITY_UNIT: TEMP_CELSIUS, - ENTITY_ICON: "mdi:thermometer", - ENTITY_CLASS: "temperature", + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ENTITY_ENABLE: False, ENTITY_API_DATA_PATH: "current_forecast:T:value", }, @@ -89,7 +93,7 @@ SENSOR_TYPES = { ENTITY_NAME: "UV", ENTITY_UNIT: None, ENTITY_ICON: "mdi:sunglasses", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "today_forecast:uv", }, @@ -97,7 +101,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Weather alert", ENTITY_UNIT: None, ENTITY_ICON: "mdi:weather-cloudy-alert", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: None, }, @@ -105,7 +109,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Daily precipitation", ENTITY_UNIT: "mm", ENTITY_ICON: "mdi:cup-water", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "today_forecast:precipitation:24h", }, @@ -113,7 +117,7 @@ SENSOR_TYPES = { ENTITY_NAME: "Cloud cover", ENTITY_UNIT: UNIT_PERCENTAGE, ENTITY_ICON: "mdi:weather-partly-cloudy", - ENTITY_CLASS: None, + ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "current_forecast:clouds", }, @@ -128,7 +132,7 @@ CONDITION_CLASSES = { "Brouillard", "Brouillard givrant", ], - "hail": ["Risque de grêle"], + "hail": ["Risque de grêle", "Risque de grèle"], "lightning": ["Risque d'orages", "Orages"], "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], "partlycloudy": [ diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 39e33dafd65..927250c6af6 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -21,7 +21,7 @@ from .const import ( COORDINATOR_RAIN, DOMAIN, ENTITY_API_DATA_PATH, - ENTITY_CLASS, + ENTITY_DEVICE_CLASS, ENTITY_ENABLE, ENTITY_ICON, ENTITY_NAME, @@ -128,7 +128,7 @@ class MeteoFranceSensor(Entity): @property def device_class(self): """Return the device class.""" - return SENSOR_TYPES[self._type][ENTITY_CLASS] + return SENSOR_TYPES[self._type][ENTITY_DEVICE_CLASS] @property def entity_registry_enabled_default(self) -> bool: @@ -170,9 +170,15 @@ class MeteoFranceRainSensor(MeteoFranceSensor): @property def state(self): """Return the state.""" - next_rain_date_locale = self.coordinator.data.next_rain_date_locale() + # search first cadran with rain + next_rain = next( + (cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1), + None, + ) return ( - dt_util.as_local(next_rain_date_locale) if next_rain_date_locale else None + dt_util.utc_from_timestamp(next_rain["dt"]).isoformat() + if next_rain + else None ) @property @@ -180,11 +186,7 @@ class MeteoFranceRainSensor(MeteoFranceSensor): """Return the state attributes.""" return { ATTR_NEXT_RAIN_1_HOUR_FORECAST: [ - { - dt_util.as_local( - self.coordinator.data.timestamp_to_locale_time(item["dt"]) - ).strftime("%H:%M"): item["desc"] - } + {dt_util.utc_from_timestamp(item["dt"]).isoformat(): item["desc"]} for item in self.coordinator.data.forecast ], ATTR_ATTRIBUTION: ATTRIBUTION, diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index a9c4840901b..e8afcea9702 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -16,6 +16,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODE, TEMP_CELSIUS from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt as dt_util from .const import ( ATTRIBUTION, @@ -134,9 +135,9 @@ class MeteoFranceWeather(WeatherEntity): continue forecast_data.append( { - ATTR_FORECAST_TIME: self.coordinator.data.timestamp_to_locale_time( + ATTR_FORECAST_TIME: dt_util.utc_from_timestamp( forecast["dt"] - ), + ).isoformat(), ATTR_FORECAST_CONDITION: format_condition( forecast["weather"]["desc"] ), From 4ecdb1f19f063a235186da39bd606c3da2f08c0b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 14 Aug 2020 15:11:41 +0200 Subject: [PATCH 164/862] Fix PR link in PR template (#38871) --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 455daf47e1e..e7412e6ba8e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -119,7 +119,7 @@ To help with the load of incoming pull requests: - [ ] I have reviewed two other [open pull requests][prs] in this repository. -[prs]: https://github.com/home-assistant/core/pulls?page=5&q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-asc+-review%3Aapproved +[prs]: https://github.com/home-assistant/core/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+-label%3Awaiting-for-upstream+sort%3Acreated-asc+-review%3Aapproved - Home Assistant Core release with the issue: From b0f214bd9c39ca9def09fca0a1512fa83e436fe2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 21 Aug 2020 15:03:44 -0500 Subject: [PATCH 266/862] Bump plexapi to 4.1.0 (#39118) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 7ea19a7a157..bbf7be9914e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.0.0", + "plexapi==4.1.0", "plexauth==0.0.5", "plexwebsocket==0.0.11" ], diff --git a/requirements_all.txt b/requirements_all.txt index ff2305d503a..ad248b7eefb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1087,7 +1087,7 @@ pillow==7.2.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.0.0 +plexapi==4.1.0 # homeassistant.components.plex plexauth==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12f5de1c5a3..2e7701148ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -510,7 +510,7 @@ pilight==0.1.1 pillow==7.2.0 # homeassistant.components.plex -plexapi==4.0.0 +plexapi==4.1.0 # homeassistant.components.plex plexauth==0.0.5 From a9ffc149f8abcd739b95a1f798e182f439a0cb2a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 21 Aug 2020 22:42:05 +0200 Subject: [PATCH 267/862] Allow templating keys in data_template (#39008) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/config_validation.py | 15 ++++++++------- homeassistant/helpers/template.py | 20 +++++++++++++++----- tests/helpers/test_script.py | 20 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f70812d5a4f..7ab81385c63 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -535,12 +535,13 @@ def template_complex(value: Any) -> Any: return_list[idx] = template_complex(element) return return_list if isinstance(value, dict): - return_dict = value.copy() - for key, element in return_dict.items(): - return_dict[key] = template_complex(element) - return return_dict - if isinstance(value, str): + return { + template_complex(key): template_complex(element) + for key, element in value.items() + } + if isinstance(value, str) and template_helper.is_template_string(value): return template(value) + return value @@ -858,7 +859,7 @@ EVENT_SCHEMA = vol.Schema( vol.Optional(CONF_ALIAS): string, vol.Required(CONF_EVENT): string, vol.Optional(CONF_EVENT_DATA): dict, - vol.Optional(CONF_EVENT_DATA_TEMPLATE): {match_all: template_complex}, + vol.Optional(CONF_EVENT_DATA_TEMPLATE): template_complex, } ) @@ -869,7 +870,7 @@ SERVICE_SCHEMA = vol.All( vol.Exclusive(CONF_SERVICE, "service name"): service, vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): template, vol.Optional("data"): dict, - vol.Optional("data_template"): {match_all: template_complex}, + vol.Optional("data_template"): template_complex, vol.Optional(CONF_ENTITY_ID): comp_entity_ids, } ), diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 00f2c4c7296..39317d873f5 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -65,8 +65,9 @@ def attach(hass: HomeAssistantType, obj: Any) -> None: for child in obj: attach(hass, child) elif isinstance(obj, dict): - for child in obj.values(): - attach(hass, child) + for child_key, child_value in obj.items(): + attach(hass, child_key) + attach(hass, child_value) elif isinstance(obj, Template): obj.hass = hass @@ -76,19 +77,28 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: if isinstance(value, list): return [render_complex(item, variables) for item in value] if isinstance(value, dict): - return {key: render_complex(item, variables) for key, item in value.items()} + return { + render_complex(key, variables): render_complex(item, variables) + for key, item in value.items() + } if isinstance(value, Template): return value.async_render(variables) + return value +def is_template_string(maybe_template: str) -> bool: + """Check if the input is a Jinja2 template.""" + return _RE_JINJA_DELIMITERS.search(maybe_template) is not None + + def extract_entities( hass: HomeAssistantType, template: Optional[str], variables: TemplateVarsType = None, ) -> Union[str, List[str]]: """Extract all entities for state_changed listener from template string.""" - if template is None or _RE_JINJA_DELIMITERS.search(template) is None: + if template is None or not is_template_string(template): return [] if _RE_NONE_ENTITIES.search(template): @@ -262,7 +272,7 @@ class Template: render_info.exception = ex finally: del self.hass.data[_RENDER_INFO] - if _RE_JINJA_DELIMITERS.search(self.template) is None: + if not is_template_string(self.template): render_info._freeze_static() else: render_info._freeze() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 1b2d190fc52..d5aa15ffe38 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -144,6 +144,26 @@ async def test_calling_service_template(hass): assert calls[0].data.get("hello") == "world" +async def test_data_template_with_templated_key(hass): + """Test the calling of a service with a data_template with a templated key.""" + context = Context() + calls = async_mock_service(hass, "test", "script") + + sequence = cv.SCRIPT_SCHEMA( + {"service": "test.script", "data_template": {"{{ hello_var }}": "world"}} + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run( + MappingProxyType({"hello_var": "hello"}), context=context + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].context is context + assert "hello" in calls[0].data + + async def test_multiple_runs_no_wait(hass): """Test multiple runs with no wait in script.""" logger = logging.getLogger("TEST") From 993088d26e93f7d753e8477eb4a77391bc36e1a2 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Fri, 21 Aug 2020 16:52:03 -0400 Subject: [PATCH 268/862] Add OZW network_statistics websocket command (#39124) * Add network_statistics websocket command * Add tests --- homeassistant/components/ozw/websocket_api.py | 21 +++++++++++++++++++ tests/components/ozw/test_websocket_api.py | 8 +++++++ tests/fixtures/ozw/generic_network_dump.csv | 3 ++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index 7f7cc489ba5..8d17b6d8d45 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -23,6 +23,7 @@ def async_register_api(hass): """Register all of our api endpoints.""" websocket_api.async_register_command(hass, websocket_get_instances) websocket_api.async_register_command(hass, websocket_network_status) + websocket_api.async_register_command(hass, websocket_network_statistics) websocket_api.async_register_command(hass, websocket_node_metadata) websocket_api.async_register_command(hass, websocket_node_status) websocket_api.async_register_command(hass, websocket_node_statistics) @@ -59,6 +60,26 @@ def websocket_network_status(hass, connection, msg): ) +@websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/network_statistics", + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + } +) +def websocket_network_statistics(hass, connection, msg): + """Get Z-Wave network statistics.""" + + manager = hass.data[DOMAIN][MANAGER] + statistics = manager.get_instance(msg[OZW_INSTANCE]).get_statistics().data + node_count = len( + manager.get_instance(msg[OZW_INSTANCE]).collections["node"].collection + ) + connection.send_result( + msg[ID], + dict(statistics, ozw_instance=msg[OZW_INSTANCE], node_count=node_count), + ) + + @websocket_api.websocket_command( { vol.Required(TYPE): "ozw/node_status", diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index a8694bb29e6..7affe62cfb4 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -74,6 +74,14 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): result = msg["result"] assert result["metadata"]["ProductPic"] == "images/aeotec/zwa002.png" + # Test network statistics + await client.send_json({ID: 9, TYPE: "ozw/network_statistics"}) + msg = await client.receive_json() + result = msg["result"] + assert result["readCnt"] == 92220 + assert result[OZW_INSTANCE] == 1 + assert result["node_count"] == 5 + async def test_refresh_node(hass, generic_data, sent_messages, hass_ws_client): """Test the ozw refresh node api.""" diff --git a/tests/fixtures/ozw/generic_network_dump.csv b/tests/fixtures/ozw/generic_network_dump.csv index a953121e881..5ca41e879ab 100644 --- a/tests/fixtures/ozw/generic_network_dump.csv +++ b/tests/fixtures/ozw/generic_network_dump.csv @@ -280,4 +280,5 @@ OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandC OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "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" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} -OpenZWave/1/node/39/statistics/,{ "sendCount": 57, "sentFailed": 0, "retries": 1, "receivedPackets": 3594, "receivedDupPackets": 12, "receivedUnsolicited": 3546, "lastSentTimeStamp": 1595764791, "lastReceivedTimeStamp": 1595802261, "lastRequestRTT": 26, "averageRequestRTT": 29, "lastResponseRTT": 38, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} \ No newline at end of file +OpenZWave/1/node/39/statistics/,{ "sendCount": 57, "sentFailed": 0, "retries": 1, "receivedPackets": 3594, "receivedDupPackets": 12, "receivedUnsolicited": 3546, "lastSentTimeStamp": 1595764791, "lastReceivedTimeStamp": 1595802261, "lastRequestRTT": 26, "averageRequestRTT": 29, "lastResponseRTT": 38, "averageResponseRTT": 37, "quality": 0, "extendedTXSupported": false, "txTime": 0, "hops": 0, "rssi_1": "", "rssi_2": "", "rssi_3": "", "rssi_4": "", "rssi_5": "", "route_1": 0, "route_2": 0, "route_3": 0, "route_4": 0, "ackChannel": 0, "lastTXChannel": 0, "routeScheme": "Idle", "routeUsed": "", "routeSpeed": "Auto", "routeTries": 0, "lastFailedLinkFrom": 0, "lastFailedLinkTo": 0} +OpenZWave/1/statistics/,{ "SOFCnt": 92220, "ACKWaiting": 0, "readAborts": 0, "badChecksum": 0, "readCnt": 92220, "writeCnt": 2150, "CANCnt": 0, "NAKCnt": 0, "ACKCnt": 2150, "OOFCnt": 0, "dropped": 27, "retries": 0, "callbacks": 1, "badroutes": 0, "noack": 18, "netbusy": 0, "notidle": 0, "txverified": 0, "nondelivery": 0, "routedbusy": 0, "broadcastReadCnt": 42190, "broadcastWriteCnt": 25} \ No newline at end of file From 83b9c6188daa373a233ec5340cbd3819f1d4b76d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 21 Aug 2020 18:31:48 -0500 Subject: [PATCH 269/862] Make template entities reloadable (#39075) * Make template entities reloadable * Address review items --- homeassistant/components/template/__init__.py | 65 +++++ .../template/alarm_control_panel.py | 15 +- .../components/template/binary_sensor.py | 14 +- homeassistant/components/template/const.py | 6 + homeassistant/components/template/cover.py | 14 +- homeassistant/components/template/fan.py | 14 +- homeassistant/components/template/light.py | 14 +- homeassistant/components/template/lock.py | 38 +-- homeassistant/components/template/sensor.py | 15 +- .../components/template/services.yaml | 3 + homeassistant/components/template/switch.py | 14 +- .../components/template/template_entity.py | 7 +- homeassistant/components/template/vacuum.py | 14 +- .../components/template/test_binary_sensor.py | 3 + tests/components/template/test_init.py | 232 ++++++++++++++++++ tests/components/template/test_sensor.py | 3 + .../template/broken_configuration.yaml | 16 ++ .../template/configuration.yaml.corrupt | Bin 0 -> 2828 bytes .../template/empty_configuration.yaml | 1 + .../template/sensor_configuration.yaml | 23 ++ 20 files changed, 468 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/template/services.yaml create mode 100644 tests/components/template/test_init.py create mode 100644 tests/fixtures/template/broken_configuration.yaml create mode 100644 tests/fixtures/template/configuration.yaml.corrupt create mode 100644 tests/fixtures/template/empty_configuration.yaml create mode 100644 tests/fixtures/template/sensor_configuration.yaml diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 0c205a0196c..f7f40cb92f7 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1 +1,66 @@ """The template component.""" + +import logging + +from homeassistant import config as conf_util +from homeassistant.const import SERVICE_RELOAD +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform, entity_platform +from homeassistant.loader import async_get_integration + +from .const import DOMAIN, EVENT_TEMPLATE_RELOADED, PLATFORM_STORAGE_KEY + +_LOGGER = logging.getLogger(__name__) + + +async def _async_setup_reload_service(hass): + if hass.services.has_service(DOMAIN, SERVICE_RELOAD): + return + + async def _reload_config(call): + """Reload the template platform config.""" + + try: + unprocessed_conf = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + for platform in hass.data[PLATFORM_STORAGE_KEY]: + + integration = await async_get_integration(hass, platform.domain) + + conf = await conf_util.async_process_component_config( + hass, unprocessed_conf, integration + ) + + if not conf: + continue + + await platform.async_reset() + + # Extract only the config for template, ignore the rest. + for p_type, p_config in config_per_platform(conf, platform.domain): + if p_type != DOMAIN: + continue + + entities = await platform.platform.async_create_entities(hass, p_config) + + await platform.async_add_entities(entities) + + hass.bus.async_fire(EVENT_TEMPLATE_RELOADED, context=call.context) + + hass.helpers.service.async_register_admin_service( + DOMAIN, SERVICE_RELOAD, _reload_config + ) + + +async def async_setup_platform_reloadable(hass): + """Template platform with reloadability.""" + + await _async_setup_reload_service(hass) + + platform = entity_platform.current_platform.get() + + if platform not in hass.data.setdefault(PLATFORM_STORAGE_KEY, []): + hass.data[PLATFORM_STORAGE_KEY].append(platform) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index f60253a17bb..ac71ec74397 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -33,6 +33,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from . import async_setup_platform_reloadable from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -75,8 +76,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Template Alarm Control Panels.""" +async def async_create_entities(hass, config): + """Create Template Alarm Control Panels.""" + alarm_control_panels = [] for device, device_config in config[CONF_ALARM_CONTROL_PANELS].items(): @@ -104,7 +106,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(alarm_control_panels) + return alarm_control_panels + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Template Alarm Control Panels.""" + + await async_setup_platform_reloadable(hass) + async_add_entities(await async_create_entities(hass, config)) class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index c2b12f8a1bc..863bf2ab1c9 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -26,6 +26,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_call_later from homeassistant.helpers.template import result_as_boolean +from . import async_setup_platform_reloadable from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -56,8 +57,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up template binary sensors.""" +async def async_create_entities(hass, config): + """Create the template binary sensors.""" sensors = [] for device, device_config in config[CONF_SENSORS].items(): @@ -90,7 +91,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(sensors) + return sensors + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the template binary sensors.""" + + await async_setup_platform_reloadable(hass) + async_add_entities(await async_create_entities(hass, config)) class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index e6cf69341f9..6d46978b86f 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -1,3 +1,9 @@ """Constants for the Template Platform Components.""" CONF_AVAILABILITY_TEMPLATE = "availability_template" + +DOMAIN = "template" + +PLATFORM_STORAGE_KEY = "template_platforms" + +EVENT_TEMPLATE_RELOADED = "event_template_reloaded" diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index ec0853894a0..688b116628c 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -37,6 +37,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from . import async_setup_platform_reloadable from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -99,8 +100,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Template cover.""" +async def async_create_entities(hass, config): + """Create the Template cover.""" covers = [] for device, device_config in config[CONF_COVERS].items(): @@ -145,7 +146,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(covers) + return covers + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Template cover.""" + + await async_setup_platform_reloadable(hass) + async_add_entities(await async_create_entities(hass, config)) class CoverTemplate(TemplateEntity, CoverEntity): diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index a6f8741aad9..747d8b522a5 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -34,6 +34,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from . import async_setup_platform_reloadable from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -80,8 +81,8 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Template Fans.""" +async def async_create_entities(hass, config): + """Create the Template Fans.""" fans = [] for device, device_config in config[CONF_FANS].items(): @@ -122,7 +123,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(fans) + return fans + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the template fans.""" + + await async_setup_platform_reloadable(hass) + async_add_entities(await async_create_entities(hass, config)) class TemplateFan(TemplateEntity, FanEntity): diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 7945c56b3f5..c066a5d66d0 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -33,6 +33,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from . import async_setup_platform_reloadable from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -77,8 +78,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Template Lights.""" +async def async_create_entities(hass, config): + """Create the Template Lights.""" lights = [] for device, device_config in config[CONF_LIGHTS].items(): @@ -128,7 +129,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(lights) + return lights + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the template lights.""" + + await async_setup_platform_reloadable(hass) + async_add_entities(await async_create_entities(hass, config)) class LightTemplate(TemplateEntity, LightEntity): diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 6097dcdb9e8..3e91e7b05c0 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -17,6 +17,7 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script +from . import async_setup_platform_reloadable from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -41,26 +42,31 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the Template lock.""" +async def async_create_entities(hass, config): + """Create the Template lock.""" device = config.get(CONF_NAME) value_template = config.get(CONF_VALUE_TEMPLATE) availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) - async_add_devices( - [ - TemplateLock( - hass, - device, - value_template, - availability_template, - config.get(CONF_LOCK), - config.get(CONF_UNLOCK), - config.get(CONF_OPTIMISTIC), - config.get(CONF_UNIQUE_ID), - ) - ] - ) + return [ + TemplateLock( + hass, + device, + value_template, + availability_template, + config.get(CONF_LOCK), + config.get(CONF_UNLOCK), + config.get(CONF_OPTIMISTIC), + config.get(CONF_UNIQUE_ID), + ) + ] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the template lock.""" + + await async_setup_platform_reloadable(hass) + async_add_entities(await async_create_entities(hass, config)) class TemplateLock(TemplateEntity, LockEntity): diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 895a8e2785b..da99ac40ed2 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -26,6 +26,7 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id +from . import async_setup_platform_reloadable from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -56,8 +57,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the template sensors.""" +async def async_create_entities(hass, config): + """Create the template sensors.""" + sensors = [] for device, device_config in config[CONF_SENSORS].items(): @@ -89,9 +91,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(sensors) + return sensors - return True + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the template sensors.""" + + await async_setup_platform_reloadable(hass) + async_add_entities(await async_create_entities(hass, config)) class SensorTemplate(TemplateEntity, Entity): diff --git a/homeassistant/components/template/services.yaml b/homeassistant/components/template/services.yaml new file mode 100644 index 00000000000..d36111d608e --- /dev/null +++ b/homeassistant/components/template/services.yaml @@ -0,0 +1,3 @@ +reload: + description: Reload all template entities. + diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index c31c89861eb..995f12584e6 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -26,6 +26,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import Script +from . import async_setup_platform_reloadable from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -54,8 +55,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Template switch.""" +async def async_create_entities(hass, config): + """Create the Template switches.""" switches = [] for device, device_config in config[CONF_SWITCHES].items(): @@ -83,7 +84,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(switches) + return switches + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the template switches.""" + + await async_setup_platform_reloadable(hass) + async_add_entities(await async_create_entities(hass, config)) class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 406ee0f9953..a65f5cdd29f 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -5,7 +5,7 @@ from typing import Any, Callable, Optional, Union import voluptuous as vol -from homeassistant.core import EVENT_HOMEASSISTANT_START, callback +from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -232,7 +232,7 @@ class TemplateEntity(Entity): attribute.async_setup() self._template_attrs.append(attribute) - async def _async_template_startup(self, _) -> None: + async def _async_template_startup(self, *_) -> None: # async_update will not write state # until "add_complete" is set on the attribute for attribute in self._template_attrs: @@ -259,6 +259,9 @@ class TemplateEntity(Entity): self.add_template_attribute( "_entity_picture", self._entity_picture_template ) + if self.hass.state == CoreState.running: + await self._async_template_startup() + return self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, self._async_template_startup diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index ce6202bdc67..3fd2c0f6ad1 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -43,6 +43,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from . import async_setup_platform_reloadable from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -92,8 +93,8 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Template Vacuums.""" +async def async_create_entities(hass, config): + """Create the Template Vacuums.""" vacuums = [] for device, device_config in config[CONF_VACUUMS].items(): @@ -138,7 +139,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ) - async_add_entities(vacuums) + return vacuums + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the template vacuums.""" + + await async_setup_platform_reloadable(hass) + async_add_entities(await async_create_entities(hass, config)) class TemplateVacuum(TemplateEntity, StateVacuumEntity): diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 45bf0c5edb0..7003b55c7ed 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.core import CoreState import homeassistant.util.dt as dt_util from tests.common import ( @@ -505,6 +506,8 @@ async def test_no_update_template_match_all(hass, caplog): """Test that we do not update sensors that match on all.""" hass.states.async_set("binary_sensor.test_sensor", "true") + hass.state = CoreState.not_running + await setup.async_setup_component( hass, "binary_sensor", diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py new file mode 100644 index 00000000000..e655fd72987 --- /dev/null +++ b/tests/components/template/test_init.py @@ -0,0 +1,232 @@ +"""The test for the Template sensor platform.""" +from os import path +from unittest.mock import patch + +from homeassistant import config +from homeassistant.components.template import DOMAIN, SERVICE_RELOAD +from homeassistant.setup import async_setup_component + + +async def test_reloadable(hass): + """Test that we can reload.""" + hass.states.async_set("sensor.test_sensor", "mytest") + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": DOMAIN, + "sensors": { + "state": { + "value_template": "{{ states.sensor.test_sensor.state }}" + }, + }, + } + }, + ) + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("sensor.state").state == "mytest" + assert len(hass.states.async_all()) == 2 + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "template/sensor_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.state") is None + assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off" + assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0 + + +async def test_reloadable_can_remove(hass): + """Test that we can reload and remove all template sensors.""" + hass.states.async_set("sensor.test_sensor", "mytest") + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": DOMAIN, + "sensors": { + "state": { + "value_template": "{{ states.sensor.test_sensor.state }}" + }, + }, + } + }, + ) + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("sensor.state").state == "mytest" + assert len(hass.states.async_all()) == 2 + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "template/empty_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + +async def test_reloadable_stops_on_invalid_config(hass): + """Test we stop the reload if configuration.yaml is completely broken.""" + hass.states.async_set("sensor.test_sensor", "mytest") + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": DOMAIN, + "sensors": { + "state": { + "value_template": "{{ states.sensor.test_sensor.state }}" + }, + }, + } + }, + ) + + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("sensor.state").state == "mytest" + assert len(hass.states.async_all()) == 2 + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "template/configuration.yaml.corrupt", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.state").state == "mytest" + assert len(hass.states.async_all()) == 2 + + +async def test_reloadable_handles_partial_valid_config(hass): + """Test we can still setup valid sensors when configuration.yaml has a broken entry.""" + hass.states.async_set("sensor.test_sensor", "mytest") + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": DOMAIN, + "sensors": { + "state": { + "value_template": "{{ states.sensor.test_sensor.state }}" + }, + }, + } + }, + ) + + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("sensor.state").state == "mytest" + assert len(hass.states.async_all()) == 2 + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "template/broken_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.state") is None + assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off" + assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0 + + +async def test_reloadable_multiple_platforms(hass): + """Test that we can reload.""" + hass.states.async_set("sensor.test_sensor", "mytest") + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": DOMAIN, + "sensors": { + "state": { + "value_template": "{{ states.sensor.test_sensor.state }}" + }, + }, + } + }, + ) + await async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": DOMAIN, + "sensors": { + "state": { + "value_template": "{{ states.sensor.test_sensor.state }}" + }, + }, + } + }, + ) + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("sensor.state").state == "mytest" + assert hass.states.get("binary_sensor.state").state == "off" + + assert len(hass.states.async_all()) == 3 + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "template/sensor_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.state") is None + assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off" + assert float(hass.states.get("sensor.combined_sensor_energy_usage").state) == 0 + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 08bf4650bba..3ffd9be4a64 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.core import CoreState from homeassistant.setup import ATTR_COMPONENT, async_setup_component, setup_component from tests.common import assert_setup_component, get_test_home_assistant @@ -584,6 +585,8 @@ async def test_no_template_match_all(hass, caplog): """Test that we allow static templates.""" hass.states.async_set("sensor.test_sensor", "startup") + hass.state = CoreState.not_running + await async_setup_component( hass, "sensor", diff --git a/tests/fixtures/template/broken_configuration.yaml b/tests/fixtures/template/broken_configuration.yaml new file mode 100644 index 00000000000..9d21081ac87 --- /dev/null +++ b/tests/fixtures/template/broken_configuration.yaml @@ -0,0 +1,16 @@ +sensor: + - platform: template + broken: + - platform: template + sensors: + combined_sensor_energy_usage: + friendly_name: Combined Sense Energy Usage + unit_of_measurement: kW + value_template: '{{ ((states(''sensor.energy_usage'') | float) + (states(''sensor.energy_usage_2'') + | float)) / 1000 }}' + watching_tv_in_master_bedroom: + friendly_name: Watching TV in Master Bedroom + value_template: '{% if state_attr("remote.alexander_master_bedroom","current_activity") + == "Watch TV" or state_attr("remote.alexander_master_bedroom","current_activity") + == "Watch Apple TV" %}on{% else %}off{% endif %}' + diff --git a/tests/fixtures/template/configuration.yaml.corrupt b/tests/fixtures/template/configuration.yaml.corrupt new file mode 100644 index 0000000000000000000000000000000000000000..b30a14ec331c8107bde73203089439bb50d6b3b3 GIT binary patch literal 2828 zcmV+n3-k2o#PBSeR?6=W_V-^O%TZc~*M!G<6rRnkvpt73(|Ekm=#zCXKN;=^AFK^L zq7TsZjuy02J`Y|wzsd8>SY4J4L{ z`zkUwZQ-p(S^01m$9&!4l6xe@nQ~cxoPy~UY;OuEfgx^@KoRdfv^lDc&iwRVQCetL zVul8MH05`qlt`dvrq@+fznKi;~kiGAGVHzU8E zvcfq{#xfSVVFoxeQ?z?WhmLy2gW|1Ys(<1p^=Z9g3ZnE*JN4>3eQT?zL~qj*(ptnZbvA!=)e4jA`t(1A^rE- zNsKF;W^*y4dZ69S8gIoQH_jOqe4wFU*wuS()!rK?i7$kx+}C-`1!98#|1g+< zN@F+neO#NGDT&%zq%IWV!>ppdeB}b9}18C#mYx z?fyhpbE4m80@Inm9#X3>zbd?o|S z^^3rtbmyDRzO8rbSd_%-Y(pRS==(^HqPRbyIPhXLTBlVJg8r{!96L2&p+{c5oGnt) zxy`^ouAQvGl&D*dLOg0*$mR-(MzF1^Wky8A`pM3?M*&?LE`w1kx*Od z3E~z4+UyG+FCSEPf;C@zn^hbW$9}4GY`S4j_Pyn=PP?J{^2%Rse?QR{B?k73lh3fo zPM>zjp8EncRV`a&dvCAb&Q&kJ2dE;9UZc5>T{HBZs~Ux8G64e6Gxs;-?uOLL#EFL0T=UIb&B2!5B->TI|-ktySb|N(fy!L>^tFZ9a6`wUpXLxKw zi-=LsKKPM$Hn4@4`9 z16lY=Y6B>;g#=Qouk!zY>|Hc1eEX5Vfd?* zJJ1f@0nazVcV7}^@_gG6k&N6zaw8lhV2RY}J!7TnPjc_gBvTUxRA4lPCEB`q4CJR9 z9}q*`QOp66n*$l49iT>`bu<>zBssp#?vY5w`z<9*3WLMd=dm|fp!(==w!eGcKIQ}m zHTKw#P5W9;8LwQByusb-HE@?6U$3W9xOLGukI$-rj_R=yIm!J}S9J~7R1aq=@Q6VS zfjf6$rUK%$Wr1te5;?JMHgi5MKx;2#&Sj=XeALF5^Lr68-e!IJQB9=3oB9XBOQLUO z;e&j{o`7%Ua;VF0#jMJ-4+O&8P#3>m>m~38f?z$eQcShWx_a9uR{;&z(3i+w`W&l}L95w!lPY z*4!`xUQ_5Ce)2KLVof%8L20}ghCQO_uO#FKofayRq;1ow695q7md_XPSi?dMON(oP zB>4n4|7?XhgZ->R@WL`GG)%fTSPkajP(^l82gx@ic%hufP0uWWBgxNeiN`!toC?lF z0fM=@yG}=S$sWc0=LY6!^64HY-3ipyUu{6{@awtltO#lHnaBu`S*X1|P!3l)XXEm? zOd|w&AT@(dU=hl6;iFzHF5~z)gW>2Z4K}cE$xW71s=<@btYfgts~s0&5^KSV3#7QV zL7s&7vN7a~FCrI;2sjp$In5*J#|*#X1TECn{I8qTFVWRphlTG*(IdESkJvb4 zui;Mjl`ny)pd(9KB4-98U5#GHS_QlE+oNLqaCJe+Q}B*TG1^7b4*Y!yb7mGz~15PBsX8Uja`rPwvzOK$$qE*b`Ro_L4 z<_g&6bypAP9cK1_^yG$9?#JcOTJNR#{5zSNW9Pz1mXD3rgfIkp)bS%3Qm7B9zG^(YvFObGV}z@cs+2z+{%#u7t#vahnkVHvsyLuRb~t zR_op%3Mz{PlG3dR$Zf9!tR&Ay1hWErT00v2-keF;774N5)!R*EWTr~A5}$aA`ofJ) z_`w#wQ@X6_pgGZ0T%!Y`_^}KLC426slIk$ zurD62k}=mDR6(feE_<%g%d~qfn)HS{RG>GTF^#7ZTBKL7mTIxp;lj8GVp7j5KV3#z zHtRXWgG)1|CoSf!*O_Lwt=Ynvzj@H6zhYV4y#8fa!af-;qKsvDm%HTa*!PKB9FrJ4 z@$J)Bhlt=8vK&7D>9uc+bFgM;2N|&K-{HfQ{PjhDfb!McdBy`5y%NrYRqI**f;b0Y zebQ!pB@BTNzEKfVhL}HTK33v;XNxIR!)v7K`7zw@5)JWCTDWKpkrnyBQ1YB)UFX9~ zYyp5g7a4mWk!9E=4fJ4|gDnhIT@1!CVty>KZX4+i=0(sKNfp(3o#-x=-Ycd8Svs#x zy{}}SbTAkCZRd5dXN}>!$}QlJOBN%jAh9^C`vzfbFFwCj6Q$mK1kENjSTxkgZK1km z>iFi?H0p}=J`w_udiehsRLqM%93-FgCuf@pfL>t3_`6K$43*#kJ(1nZGw9~=1^Ad< z#f^H9Wms_1K>dzS$B}@A!Z~D9&@#tzYlbN(DJ5gY*j#<NjcGF^;s!sDSF2J>V_gXs*{Hkz3W_hbZ%;m=FZj&KML{7 zlNiG Date: Sat, 22 Aug 2020 07:49:09 +0300 Subject: [PATCH 270/862] Add Risco integration (#36930) * Risco integration * Fix lint errors * Raise ConfigEntryNotReady if can't connect * Gracefully handle shutdown * pass session to pyrisco * minor change to init * Fix retries * Add exception log * Remove retries * Address code review comments * Remove log --- CODEOWNERS | 1 + homeassistant/components/risco/__init__.py | 89 +++++++ .../components/risco/alarm_control_panel.py | 162 ++++++++++++ homeassistant/components/risco/config_flow.py | 58 +++++ homeassistant/components/risco/const.py | 5 + homeassistant/components/risco/manifest.json | 12 + homeassistant/components/risco/strings.json | 21 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/risco/__init__.py | 1 + .../risco/test_alarm_control_panel.py | 237 ++++++++++++++++++ tests/components/risco/test_config_flow.py | 113 +++++++++ 13 files changed, 706 insertions(+) create mode 100644 homeassistant/components/risco/__init__.py create mode 100644 homeassistant/components/risco/alarm_control_panel.py create mode 100644 homeassistant/components/risco/config_flow.py create mode 100644 homeassistant/components/risco/const.py create mode 100644 homeassistant/components/risco/manifest.json create mode 100644 homeassistant/components/risco/strings.json create mode 100644 tests/components/risco/__init__.py create mode 100644 tests/components/risco/test_alarm_control_panel.py create mode 100644 tests/components/risco/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 6009cd55745..7ce722b8617 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -346,6 +346,7 @@ homeassistant/components/random/* @fabaff homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen @elupus homeassistant/components/ring/* @balloob +homeassistant/components/risco/* @OnFreund homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roku/* @ctalkington homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py new file mode 100644 index 00000000000..d620e2c1c68 --- /dev/null +++ b/homeassistant/components/risco/__init__.py @@ -0,0 +1,89 @@ +"""The Risco integration.""" +import asyncio +from datetime import timedelta +import logging + +from pyrisco import CannotConnectError, OperationError, RiscoAPI, UnauthorizedError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DATA_COORDINATOR, DOMAIN + +PLATFORMS = ["alarm_control_panel"] + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Risco component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Risco from a config entry.""" + data = entry.data + risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) + try: + await risco.login(async_get_clientsession(hass)) + except CannotConnectError as error: + raise ConfigEntryNotReady() from error + except UnauthorizedError: + _LOGGER.exception("Failed to login to Risco cloud") + return False + + coordinator = RiscoDataUpdateCoordinator(hass, risco) + await coordinator.async_refresh() + + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinator, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class RiscoDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching risco data.""" + + def __init__(self, hass, risco): + """Initialize global risco data updater.""" + self.risco = risco + interval = timedelta(seconds=30) + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=interval, + ) + + async def _async_update_data(self): + """Fetch data from risco.""" + try: + return await self.risco.get_state() + except (CannotConnectError, UnauthorizedError, OperationError) as error: + raise UpdateFailed from error diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py new file mode 100644 index 00000000000..a92b3cc186a --- /dev/null +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -0,0 +1,162 @@ +"""Support for Risco alarms.""" +import logging + +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_UNKNOWN, +) + +from .const import DATA_COORDINATOR, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SUPPORTED_STATES = [ + STATE_ALARM_DISARMED, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_TRIGGERED, +] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Risco alarm control panel.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + entities = [ + RiscoAlarm(hass, coordinator, partition_id) + for partition_id in coordinator.data.partitions.keys() + ] + + async_add_entities(entities, False) + + +class RiscoAlarm(AlarmControlPanelEntity): + """Representation of a Risco partition.""" + + def __init__(self, hass, coordinator, partition_id): + """Init the partition.""" + self._hass = hass + self._coordinator = coordinator + self._partition_id = partition_id + self._partition = self._coordinator.data.partitions[self._partition_id] + + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @property + def available(self): + """Return if entity is available.""" + return self._coordinator.last_update_success + + def _refresh_from_coordinator(self): + self._partition = self._coordinator.data.partitions[self._partition_id] + self.async_write_ha_state() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self._refresh_from_coordinator) + ) + + @property + def _risco(self): + """Return the Risco API object.""" + return self._coordinator.risco + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Risco", + } + + @property + def name(self): + """Return the name of the partition.""" + return f"Risco {self._risco.site_name} Partition {self._partition_id}" + + @property + def unique_id(self): + """Return a unique id for that partition.""" + return f"{self._risco.site_uuid}_{self._partition_id}" + + @property + def state(self): + """Return the state of the device.""" + if self._partition.triggered: + return STATE_ALARM_TRIGGERED + if self._partition.arming: + return STATE_ALARM_ARMING + if self._partition.armed: + return STATE_ALARM_ARMED_AWAY + if self._partition.partially_armed: + return STATE_ALARM_ARMED_HOME + if self._partition.disarmed: + return STATE_ALARM_DISARMED + + return STATE_UNKNOWN + + @property + def supported_features(self): + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + ) + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return False + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + await self._call_alarm_method("disarm") + + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + await self._call_alarm_method("partial_arm") + + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + await self._call_alarm_method("partial_arm") + + async def async_alarm_arm_custom_bypass(self, code=None): + """Send arm custom bypass command.""" + await self._call_alarm_method("partial_arm") + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + await self._call_alarm_method("arm") + + async def _call_alarm_method(self, method, code=None): + alarm = await getattr(self._risco, method)(self._partition_id) + self._partition = alarm.partitions[self._partition_id] + self.async_write_ha_state() + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py new file mode 100644 index 00000000000..75807a63406 --- /dev/null +++ b/homeassistant/components/risco/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow for Risco integration.""" +import logging + +from pyrisco import CannotConnectError, RiscoAPI, UnauthorizedError +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str, CONF_PIN: str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) + + try: + await risco.login(async_get_clientsession(hass)) + finally: + await risco.close() + + return {"title": risco.site_name} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Risco.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnectError: + errors["base"] = "cannot_connect" + except UnauthorizedError: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py new file mode 100644 index 00000000000..7fa85227fe1 --- /dev/null +++ b/homeassistant/components/risco/const.py @@ -0,0 +1,5 @@ +"""Constants for the Risco integration.""" + +DOMAIN = "risco" + +DATA_COORDINATOR = "risco" diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json new file mode 100644 index 00000000000..9dd6fd95680 --- /dev/null +++ b/homeassistant/components/risco/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "risco", + "name": "Risco", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/risco", + "requirements": [ + "pyrisco==0.2.1" + ], + "codeowners": [ + "@OnFreund" + ] +} \ No newline at end of file diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json new file mode 100644 index 00000000000..a2ce75a9d74 --- /dev/null +++ b/homeassistant/components/risco/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "pin": "Pin code" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8a9343cf58b..e1ab7647446 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -144,6 +144,7 @@ FLOWS = [ "rachio", "rainmachine", "ring", + "risco", "roku", "roomba", "roon", diff --git a/requirements_all.txt b/requirements_all.txt index ad248b7eefb..47aa702fcd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1592,6 +1592,9 @@ pyrecswitch==1.0.2 # homeassistant.components.repetier pyrepetier==3.0.5 +# homeassistant.components.risco +pyrisco==0.2.1 + # homeassistant.components.sabnzbd pysabnzbd==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e7701148ab..3f7d1f9259e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -754,6 +754,9 @@ pyps4-2ndscreen==1.1.1 # homeassistant.components.qwikswitch pyqwikswitch==0.93 +# homeassistant.components.risco +pyrisco==0.2.1 + # homeassistant.components.acer_projector # homeassistant.components.zha pyserial==3.4 diff --git a/tests/components/risco/__init__.py b/tests/components/risco/__init__.py new file mode 100644 index 00000000000..1a84a8d2399 --- /dev/null +++ b/tests/components/risco/__init__.py @@ -0,0 +1 @@ +"""Tests for the Risco integration.""" diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py new file mode 100644 index 00000000000..574e422eda1 --- /dev/null +++ b/tests/components/risco/test_alarm_control_panel.py @@ -0,0 +1,237 @@ +"""Tests for the Risco alarm control panel device.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.components.risco import CannotConnectError, UnauthorizedError +from homeassistant.components.risco.const import DOMAIN +from homeassistant.const import ( + CONF_PASSWORD, + CONF_PIN, + CONF_USERNAME, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_UNKNOWN, +) +from homeassistant.helpers.entity_component import async_update_entity + +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch +from tests.common import MockConfigEntry + +TEST_CONFIG = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PIN: "1234", +} +TEST_SITE_UUID = "test-site-uuid" +TEST_SITE_NAME = "test-site-name" +FIRST_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0" +SECOND_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_1" + + +def _partition_mock(): + return MagicMock( + triggered=False, + arming=False, + armed=False, + disarmed=False, + partially_armed=False, + ) + + +@pytest.fixture +def two_part_alarm(): + """Fixture to mock alarm with two partitions.""" + partition_mocks = {0: _partition_mock(), 1: _partition_mock()} + alarm_mock = MagicMock() + with patch.object( + partition_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), patch.object( + partition_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), patch.object( + alarm_mock, + "partitions", + new_callable=PropertyMock(return_value=partition_mocks), + ), patch( + "homeassistant.components.risco.RiscoAPI.get_state", + AsyncMock(return_value=alarm_mock), + ): + yield alarm_mock + + +async def _setup_risco(hass, alarm=MagicMock()): + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + with patch( + "homeassistant.components.risco.RiscoAPI.login", return_value=True, + ), patch( + "homeassistant.components.risco.RiscoAPI.site_uuid", + new_callable=PropertyMock(return_value=TEST_SITE_UUID), + ), patch( + "homeassistant.components.risco.RiscoAPI.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.RiscoAPI.close", AsyncMock() + ): + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +async def test_cannot_connect(hass): + """Test connection error.""" + + with patch( + "homeassistant.components.risco.RiscoAPI.login", side_effect=CannotConnectError, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + registry = await hass.helpers.entity_registry.async_get_registry() + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + +async def test_unauthorized(hass): + """Test unauthorized error.""" + + with patch( + "homeassistant.components.risco.RiscoAPI.login", side_effect=UnauthorizedError, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + registry = await hass.helpers.entity_registry.async_get_registry() + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + +async def test_setup(hass, two_part_alarm): + """Test entity setup.""" + registry = await hass.helpers.entity_registry.async_get_registry() + + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + await _setup_risco(hass, two_part_alarm) + + assert registry.async_is_registered(FIRST_ENTITY_ID) + assert registry.async_is_registered(SECOND_ENTITY_ID) + + registry = await hass.helpers.device_registry.async_get_registry() + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0")}, {}) + assert device is not None + assert device.manufacturer == "Risco" + + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1")}, {}) + assert device is not None + assert device.manufacturer == "Risco" + + +async def _check_state(hass, alarm, property, state, entity_id, partition_id): + with patch.object(alarm.partitions[partition_id], property, return_value=True): + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == state + + +async def test_states(hass, two_part_alarm): + """Test the various alarm states.""" + await _setup_risco(hass, two_part_alarm) + + assert hass.states.get(FIRST_ENTITY_ID).state == STATE_UNKNOWN + await _check_state( + hass, two_part_alarm, "triggered", STATE_ALARM_TRIGGERED, FIRST_ENTITY_ID, 0 + ) + await _check_state( + hass, two_part_alarm, "triggered", STATE_ALARM_TRIGGERED, SECOND_ENTITY_ID, 1 + ) + await _check_state( + hass, two_part_alarm, "arming", STATE_ALARM_ARMING, FIRST_ENTITY_ID, 0 + ) + await _check_state( + hass, two_part_alarm, "arming", STATE_ALARM_ARMING, SECOND_ENTITY_ID, 1 + ) + await _check_state( + hass, two_part_alarm, "armed", STATE_ALARM_ARMED_AWAY, FIRST_ENTITY_ID, 0 + ) + await _check_state( + hass, two_part_alarm, "armed", STATE_ALARM_ARMED_AWAY, SECOND_ENTITY_ID, 1 + ) + await _check_state( + hass, + two_part_alarm, + "partially_armed", + STATE_ALARM_ARMED_HOME, + FIRST_ENTITY_ID, + 0, + ) + await _check_state( + hass, + two_part_alarm, + "partially_armed", + STATE_ALARM_ARMED_HOME, + SECOND_ENTITY_ID, + 1, + ) + await _check_state( + hass, two_part_alarm, "disarmed", STATE_ALARM_DISARMED, FIRST_ENTITY_ID, 0 + ) + await _check_state( + hass, two_part_alarm, "disarmed", STATE_ALARM_DISARMED, SECOND_ENTITY_ID, 1 + ) + + +async def _test_servie_call(hass, service, method, entity_id, partition_id): + with patch( + "homeassistant.components.risco.RiscoAPI." + method, AsyncMock() + ) as set_mock: + await _call_alarm_service(hass, service, entity_id) + set_mock.assert_awaited_once_with(partition_id) + + +async def _call_alarm_service(hass, service, entity_id): + data = {"entity_id": entity_id} + + await hass.services.async_call( + ALARM_DOMAIN, service, service_data=data, blocking=True + ) + + +async def test_sets(hass, two_part_alarm): + """Test settings the various modes.""" + await _setup_risco(hass, two_part_alarm) + + await _test_servie_call(hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0) + await _test_servie_call(hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1) + await _test_servie_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0) + await _test_servie_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1) + await _test_servie_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0 + ) + await _test_servie_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1 + ) + await _test_servie_call( + hass, SERVICE_ALARM_ARM_NIGHT, "partial_arm", FIRST_ENTITY_ID, 0 + ) + await _test_servie_call( + hass, SERVICE_ALARM_ARM_NIGHT, "partial_arm", SECOND_ENTITY_ID, 1 + ) + await _test_servie_call( + hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", FIRST_ENTITY_ID, 0 + ) + await _test_servie_call( + hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", SECOND_ENTITY_ID, 1 + ) diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py new file mode 100644 index 00000000000..8886930100a --- /dev/null +++ b/tests/components/risco/test_config_flow.py @@ -0,0 +1,113 @@ +"""Test the Risco config flow.""" +from homeassistant import config_entries, setup +from homeassistant.components.risco.config_flow import ( + CannotConnectError, + UnauthorizedError, +) +from homeassistant.components.risco.const import DOMAIN + +from tests.async_mock import AsyncMock, PropertyMock, patch + +TEST_SITE_NAME = "test-site-name" +TEST_DATA = { + "username": "test-username", + "password": "test-password", + "pin": "1234", +} + + +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.risco.config_flow.RiscoAPI.login", return_value=True, + ), patch( + "homeassistant.components.risco.config_flow.RiscoAPI.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock() + ) as mock_close, patch( + "homeassistant.components.risco.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.risco.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == TEST_SITE_NAME + assert result2["data"] == TEST_DATA + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + mock_close.assert_awaited_once() + + +async def test_form_invalid_auth(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.risco.config_flow.RiscoAPI.login", + side_effect=UnauthorizedError, + ), patch( + "homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock() + ) as mock_close: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + mock_close.assert_awaited_once() + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.risco.config_flow.RiscoAPI.login", + side_effect=CannotConnectError, + ), patch( + "homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock() + ) as mock_close: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + mock_close.assert_awaited_once() + + +async def test_form_exception(hass): + """Test we handle unknown exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.risco.config_flow.RiscoAPI.login", + side_effect=Exception, + ), patch( + "homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock() + ) as mock_close: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + mock_close.assert_awaited_once() From 18047726b692f9e8d1a6f13dc756ec6342f9fa34 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 22 Aug 2020 09:01:17 +0200 Subject: [PATCH 271/862] Remove ending period from cast log --- homeassistant/components/cast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 7cd4697558c..c4588b3c4c3 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -377,7 +377,7 @@ class CastDevice(MediaPlayerEntity): _LOGGER.error( "Failed to cast media %s%s. Please make sure the URL is: " "Reachable from the cast device and either a publicly resolvable " - "hostname or an IP address.", + "hostname or an IP address", media_status.content_id, url_description, ) From 45401d43087fe15c800e442990c6d3d136be9c9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2020 02:35:18 -0500 Subject: [PATCH 272/862] Fix flapping recorder last run test (#39134) --- tests/components/recorder/test_util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index d56b289f44b..14b26b8c8e3 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -9,6 +9,8 @@ from homeassistant.components.recorder import util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.util import dt as dt_util +from .common import wait_recording_done + from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant, init_recorder_component @@ -101,6 +103,7 @@ def test_last_run_was_recently_clean(hass_recorder): assert util.last_run_was_recently_clean(cursor) is False hass.data[DATA_INSTANCE]._close_run() + wait_recording_done(hass) assert util.last_run_was_recently_clean(cursor) is True From e3ce699d75df56bf44a1e3c9de33d8950734f3fd Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 22 Aug 2020 16:54:12 +0300 Subject: [PATCH 273/862] Risco code review follow ups (#39143) --- .../components/risco/alarm_control_panel.py | 31 +++----------- homeassistant/components/risco/config_flow.py | 7 +++- .../risco/test_alarm_control_panel.py | 28 +++---------- tests/components/risco/test_config_flow.py | 41 +++++++++++++------ 4 files changed, 44 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index a92b3cc186a..096c4023ffe 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -4,19 +4,14 @@ import logging from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_CUSTOM_BYPASS, SUPPORT_ALARM_ARM_HOME, - SUPPORT_ALARM_ARM_NIGHT, ) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, - STATE_UNKNOWN, ) from .const import DATA_COORDINATOR, DOMAIN @@ -27,8 +22,6 @@ SUPPORTED_STATES = [ STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED, ] @@ -37,8 +30,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Risco alarm control panel.""" coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] entities = [ - RiscoAlarm(hass, coordinator, partition_id) - for partition_id in coordinator.data.partitions.keys() + RiscoAlarm(coordinator, partition_id) + for partition_id in coordinator.data.partitions ] async_add_entities(entities, False) @@ -47,9 +40,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RiscoAlarm(AlarmControlPanelEntity): """Representation of a Risco partition.""" - def __init__(self, hass, coordinator, partition_id): + def __init__(self, coordinator, partition_id): """Init the partition.""" - self._hass = hass self._coordinator = coordinator self._partition_id = partition_id self._partition = self._coordinator.data.partitions[self._partition_id] @@ -112,17 +104,12 @@ class RiscoAlarm(AlarmControlPanelEntity): if self._partition.disarmed: return STATE_ALARM_DISARMED - return STATE_UNKNOWN + return None @property def supported_features(self): """Return the list of supported features.""" - return ( - SUPPORT_ALARM_ARM_HOME - | SUPPORT_ALARM_ARM_AWAY - | SUPPORT_ALARM_ARM_NIGHT - | SUPPORT_ALARM_ARM_CUSTOM_BYPASS - ) + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY @property def code_arm_required(self): @@ -137,14 +124,6 @@ class RiscoAlarm(AlarmControlPanelEntity): """Send arm home command.""" await self._call_alarm_method("partial_arm") - async def async_alarm_arm_night(self, code=None): - """Send arm night command.""" - await self._call_alarm_method("partial_arm") - - async def async_alarm_arm_custom_bypass(self, code=None): - """Send arm custom bypass command.""" - await self._call_alarm_method("partial_arm") - async def async_alarm_arm_away(self, code=None): """Send arm away command.""" await self._call_alarm_method("arm") diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 75807a63406..43fb75343cb 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -41,10 +41,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() + try: info = await validate_input(self.hass, user_input) - - return self.async_create_entry(title=info["title"], data=user_input) except CannotConnectError: errors["base"] = "cannot_connect" except UnauthorizedError: @@ -52,6 +53,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 574e422eda1..0219ceca7bd 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -9,9 +9,7 @@ from homeassistant.const import ( CONF_PIN, CONF_USERNAME, SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_CUSTOM_BYPASS, SERVICE_ALARM_ARM_HOME, - SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -22,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity_component import async_update_entity -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry TEST_CONFIG = { @@ -60,14 +58,15 @@ def two_part_alarm(): "partitions", new_callable=PropertyMock(return_value=partition_mocks), ), patch( - "homeassistant.components.risco.RiscoAPI.get_state", - AsyncMock(return_value=alarm_mock), + "homeassistant.components.risco.RiscoAPI.get_state", return_value=alarm_mock, ): yield alarm_mock async def _setup_risco(hass, alarm=MagicMock()): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + config_entry.add_to_hass(hass) + with patch( "homeassistant.components.risco.RiscoAPI.login", return_value=True, ), patch( @@ -77,9 +76,8 @@ async def _setup_risco(hass, alarm=MagicMock()): "homeassistant.components.risco.RiscoAPI.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.RiscoAPI.close", AsyncMock() + "homeassistant.components.risco.RiscoAPI.close" ): - config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -194,9 +192,7 @@ async def test_states(hass, two_part_alarm): async def _test_servie_call(hass, service, method, entity_id, partition_id): - with patch( - "homeassistant.components.risco.RiscoAPI." + method, AsyncMock() - ) as set_mock: + with patch("homeassistant.components.risco.RiscoAPI." + method) as set_mock: await _call_alarm_service(hass, service, entity_id) set_mock.assert_awaited_once_with(partition_id) @@ -223,15 +219,3 @@ async def test_sets(hass, two_part_alarm): await _test_servie_call( hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1 ) - await _test_servie_call( - hass, SERVICE_ALARM_ARM_NIGHT, "partial_arm", FIRST_ENTITY_ID, 0 - ) - await _test_servie_call( - hass, SERVICE_ALARM_ARM_NIGHT, "partial_arm", SECOND_ENTITY_ID, 1 - ) - await _test_servie_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", FIRST_ENTITY_ID, 0 - ) - await _test_servie_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", SECOND_ENTITY_ID, 1 - ) diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 8886930100a..259f5dfe15d 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -1,12 +1,13 @@ """Test the Risco config flow.""" -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.risco.config_flow import ( CannotConnectError, UnauthorizedError, ) from homeassistant.components.risco.const import DOMAIN -from tests.async_mock import AsyncMock, PropertyMock, patch +from tests.async_mock import PropertyMock, patch +from tests.common import MockConfigEntry TEST_SITE_NAME = "test-site-name" TEST_DATA = { @@ -18,7 +19,6 @@ TEST_DATA = { 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} ) @@ -31,7 +31,7 @@ async def test_form(hass): "homeassistant.components.risco.config_flow.RiscoAPI.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock() + "homeassistant.components.risco.config_flow.RiscoAPI.close" ) as mock_close, patch( "homeassistant.components.risco.async_setup", return_value=True ) as mock_setup, patch( @@ -59,9 +59,7 @@ async def test_form_invalid_auth(hass): with patch( "homeassistant.components.risco.config_flow.RiscoAPI.login", side_effect=UnauthorizedError, - ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock() - ) as mock_close: + ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA ) @@ -80,9 +78,7 @@ async def test_form_cannot_connect(hass): with patch( "homeassistant.components.risco.config_flow.RiscoAPI.login", side_effect=CannotConnectError, - ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock() - ) as mock_close: + ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA ) @@ -101,9 +97,7 @@ async def test_form_exception(hass): with patch( "homeassistant.components.risco.config_flow.RiscoAPI.login", side_effect=Exception, - ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock() - ) as mock_close: + ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA ) @@ -111,3 +105,24 @@ async def test_form_exception(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} mock_close.assert_awaited_once() + + +async def test_form_already_exists(hass): + """Test that a flow with an existing username aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id=TEST_DATA["username"], data=TEST_DATA, + ) + + entry.add_to_hass(hass) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" From 55c71b5e83f7d073f71c4d5bfc1ad3994db18759 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 22 Aug 2020 12:09:20 -0400 Subject: [PATCH 274/862] Fix Vizio pylance error by using schema extend instead of dict update (#39139) * fix pylance error by using schema extend instead of dict update * fix bug --- homeassistant/components/vizio/config_flow.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index fcb41a5891b..ba7bb39f7ea 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -123,14 +123,16 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): return self.async_create_entry(title="", data=user_input) - options = { - vol.Optional( - CONF_VOLUME_STEP, - default=self.config_entry.options.get( - CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP - ), - ): vol.All(vol.Coerce(int), vol.Range(min=1, max=10)) - } + options = vol.Schema( + { + vol.Optional( + CONF_VOLUME_STEP, + default=self.config_entry.options.get( + CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP + ), + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=10)) + } + ) if self.config_entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV: default_include_or_exclude = ( @@ -139,7 +141,7 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): and CONF_EXCLUDE in self.config_entry.options.get(CONF_APPS, {}) else CONF_INCLUDE ) - options.update( + options = options.extend( { vol.Optional( CONF_INCLUDE_OR_EXCLUDE, @@ -156,7 +158,7 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): } ) - return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + return self.async_show_form(step_id="init", data_schema=options) class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): From e90959658744fbcaf50b726d0d54bf1d1eb33a4b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 22 Aug 2020 18:12:09 +0200 Subject: [PATCH 275/862] Upgrade volkszaehler to 0.1.3 (#39147) --- CODEOWNERS | 1 + homeassistant/components/volkszaehler/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7ce722b8617..70007e16539 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -465,6 +465,7 @@ homeassistant/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf +homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index 0e28675ce87..b133550b327 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -2,6 +2,6 @@ "domain": "volkszaehler", "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", - "requirements": ["volkszaehler==0.1.2"], - "codeowners": [] + "requirements": ["volkszaehler==0.1.3"], + "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index 47aa702fcd5..2b9158ad3b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2199,7 +2199,7 @@ venstarcolortouch==0.12 vilfo-api-client==0.3.2 # homeassistant.components.volkszaehler -volkszaehler==0.1.2 +volkszaehler==0.1.3 # homeassistant.components.volvooncall volvooncall==0.8.12 From 4af90e41ce1b265e36a48a499656d5f6a5563833 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 22 Aug 2020 18:15:30 +0200 Subject: [PATCH 276/862] Upgrade pylast to 3.3.0 (#39151) --- homeassistant/components/lastfm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index 85d6c5ea8d2..1bc38cb0359 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -2,6 +2,6 @@ "domain": "lastfm", "name": "Last.fm", "documentation": "https://www.home-assistant.io/integrations/lastfm", - "requirements": ["pylast==3.2.1"], + "requirements": ["pylast==3.3.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 2b9158ad3b5..70c69aba98c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1438,7 +1438,7 @@ pykwb==0.0.8 pylacrosse==0.4.0 # homeassistant.components.lastfm -pylast==3.2.1 +pylast==3.3.0 # homeassistant.components.launch_library pylaunches==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f7d1f9259e..e0c8786ebd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -687,7 +687,7 @@ pykira==0.1.1 pykodi==0.1 # homeassistant.components.lastfm -pylast==3.2.1 +pylast==3.3.0 # homeassistant.components.forked_daapd pylibrespot-java==0.1.0 From cdb6161d3dde94c22266a99547d5652ad8946dc2 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 22 Aug 2020 20:15:03 +0300 Subject: [PATCH 277/862] Add risco options flow (#39154) --- homeassistant/components/risco/__init__.py | 27 ++++++++++--- homeassistant/components/risco/config_flow.py | 38 ++++++++++++++++++- homeassistant/components/risco/const.py | 2 + homeassistant/components/risco/strings.json | 10 +++++ tests/components/risco/test_config_flow.py | 27 ++++++++++++- 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index d620e2c1c68..e3e59229bf5 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -6,16 +6,21 @@ import logging from pyrisco import CannotConnectError, OperationError, RiscoAPI, UnauthorizedError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.const import ( + CONF_PASSWORD, + CONF_PIN, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_COORDINATOR, DOMAIN +from .const import DATA_COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN PLATFORMS = ["alarm_control_panel"] - +UNDO_UPDATE_LISTENER = "undo_update_listener" _LOGGER = logging.getLogger(__name__) @@ -38,11 +43,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.exception("Failed to login to Risco cloud") return False - coordinator = RiscoDataUpdateCoordinator(hass, risco) + scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + coordinator = RiscoDataUpdateCoordinator(hass, risco, scan_interval) await coordinator.async_refresh() + undo_listener = entry.add_update_listener(_update_listener) + hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, + UNDO_UPDATE_LISTENER: undo_listener, } for component in PLATFORMS: @@ -65,18 +74,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) if unload_ok: + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + class RiscoDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching risco data.""" - def __init__(self, hass, risco): + def __init__(self, hass, risco, scan_interval): """Initialize global risco data updater.""" self.risco = risco - interval = timedelta(seconds=30) + interval = timedelta(seconds=scan_interval) super().__init__( hass, _LOGGER, name=DOMAIN, update_interval=interval, ) diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 43fb75343cb..af2df0ca577 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -5,10 +5,15 @@ from pyrisco import CannotConnectError, RiscoAPI, UnauthorizedError import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME +from homeassistant.const import ( + CONF_PASSWORD, + CONF_PIN, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN # pylint:disable=unused-import +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -37,6 +42,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + @staticmethod + @core.callback + def async_get_options_flow(config_entry): + """Define the config flow to handle options.""" + return RiscoOptionsFlowHandler(config_entry) + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} @@ -59,3 +70,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + + +class RiscoOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a Risco options flow.""" + + def __init__(self, config_entry): + """Initialize.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry( + title="", data={CONF_SCAN_INTERVAL: user_input[CONF_SCAN_INTERVAL]} + ) + + current = self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + + options = vol.Schema({vol.Required(CONF_SCAN_INTERVAL, default=current): int}) + + return self.async_show_form(step_id="init", data_schema=options) diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index 7fa85227fe1..0beb3b491db 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -3,3 +3,5 @@ DOMAIN = "risco" DATA_COORDINATOR = "risco" + +DEFAULT_SCAN_INTERVAL = 30 diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json index a2ce75a9d74..839f4d67251 100644 --- a/homeassistant/components/risco/strings.json +++ b/homeassistant/components/risco/strings.json @@ -17,5 +17,15 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "title": "Configure options", + "data": { + "scan_interval": "How often to poll Risco (in seconds)" + } + } + } } } \ No newline at end of file diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 259f5dfe15d..a3540fd1b1e 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Risco config flow.""" -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.risco.config_flow import ( CannotConnectError, UnauthorizedError, @@ -114,7 +114,6 @@ async def test_form_already_exists(hass): ) entry.add_to_hass(hass) - await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -126,3 +125,27 @@ async def test_form_already_exists(hass): assert result2["type"] == "abort" assert result2["reason"] == "already_configured" + + +async def test_options_flow(hass): + """Test options flow.""" + conf = {"scan_interval": 10} + + entry = MockConfigEntry( + domain=DOMAIN, unique_id=TEST_DATA["username"], data=TEST_DATA, + ) + + entry.add_to_hass(hass) + + with patch("homeassistant.components.risco.async_setup_entry", return_value=True): + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input=conf, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == conf From 3c1c6069dae1695ccbda30e2257f5b58b1844ac8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 22 Aug 2020 19:51:39 +0200 Subject: [PATCH 278/862] Fix Sentry user context and system info (#39130) --- homeassistant/components/sentry/__init__.py | 17 ++++++++++++++++- tests/components/sentry/test_init.py | 3 +++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 56eede297ab..62f7af8b900 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -8,7 +8,10 @@ from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from homeassistant.config_entries import ConfigEntry -from homeassistant.const import __version__ as current_version +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, + __version__ as current_version, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.loader import Integration, async_get_custom_components @@ -96,6 +99,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: **tracing, ) + async def update_system_info(now): + nonlocal system_info + system_info = await hass.helpers.system_info.async_get_system_info() + + # Update system info every hour + hass.helpers.event.async_call_later(3600, update_system_info) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, update_system_info) + return True @@ -186,6 +198,9 @@ def process_before_send( # Update event with the additional tags event.setdefault("tags", {}).update(additional_tags) + # Set user context to the installation UUID + event.setdefault("user", {}).update({"id": huuid}) + # Update event data with Home Assistant Context event.setdefault("contexts", {}).update( { diff --git a/tests/components/sentry/test_init.py b/tests/components/sentry/test_init.py index 5f89b129aca..6c3555092f0 100644 --- a/tests/components/sentry/test_init.py +++ b/tests/components/sentry/test_init.py @@ -154,6 +154,9 @@ async def test_process_before_send(hass: HomeAssistant): assert tags["uuid"] == "12345" assert tags["installation_type"] == "pytest" + user = result["user"] + assert user["id"] == "12345" + async def test_event_with_platform_context(hass: HomeAssistant): """Test extraction of platform context information during Sentry events.""" From f075742a8632f311db0dcb03c78ccae6e559efe6 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 22 Aug 2020 21:40:12 +0300 Subject: [PATCH 279/862] Address Kodi code review follow up (#39104) * Code review follow up * Update config_flow.py * Update config_flow.py * Update strings.json * Update config_flow.py * Update config_flow.py * Update config_flow.py * Update config_flow.py * Update config_flow.py * Update config_flow.py * Update util.py * Update test_config_flow.py * Update config_flow.py * Update util.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update util.py * Update test_config_flow.py * Update config_flow.py * Update config_flow.py * Update config_flow.py * Update config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py Co-authored-by: Chris Talkington --- homeassistant/components/kodi/config_flow.py | 239 ++++++---- homeassistant/components/kodi/strings.json | 8 +- tests/components/kodi/__init__.py | 3 +- tests/components/kodi/test_config_flow.py | 433 +++++++++++++++---- tests/components/kodi/util.py | 41 ++ 5 files changed, 536 insertions(+), 188 deletions(-) diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 0e1a94821e6..34e08ff56e4 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -40,7 +40,7 @@ async def validate_http(hass: core.HomeAssistant, data): ssl = data.get(CONF_SSL) session = async_get_clientsession(hass) - _LOGGER.debug("Connecting to %s:%s over HTTP.", host, port) + _LOGGER.debug("Connecting to %s:%s over HTTP", host, port) khc = get_kodi_connection( host, port, None, username, password, ssl, session=session ) @@ -67,19 +67,19 @@ async def validate_ws(hass: core.HomeAssistant, data): session = async_get_clientsession(hass) - _LOGGER.debug("Connecting to %s:%s over WebSocket.", host, ws_port) + _LOGGER.debug("Connecting to %s:%s over WebSocket", host, ws_port) kwc = get_kodi_connection( host, port, ws_port, username, password, ssl, session=session ) try: await kwc.connect() if not kwc.connected: - _LOGGER.warning("Cannot connect to %s:%s over WebSocket.", host, ws_port) - raise CannotConnect() + _LOGGER.warning("Cannot connect to %s:%s over WebSocket", host, ws_port) + raise WSCannotConnect() kodi = Kodi(kwc) await kodi.ping() except CannotConnectError as error: - raise CannotConnect from error + raise WSCannotConnect from error class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -91,8 +91,8 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize flow.""" self._host: Optional[str] = None - self._port: Optional[int] = None - self._ws_port: Optional[int] = None + self._port: Optional[int] = DEFAULT_PORT + self._ws_port: Optional[int] = DEFAULT_WS_PORT self._name: Optional[str] = None self._username: Optional[str] = None self._password: Optional[str] = None @@ -116,83 +116,170 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) + try: + await validate_http(self.hass, self._get_data()) + await validate_ws(self.hass, self._get_data()) + except InvalidAuth: + return await self.async_step_credentials() + except WSCannotConnect: + return await self.async_step_ws_port() + except CannotConnect: + return self.async_abort(reason="cannot_connect") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update({"title_placeholders": {CONF_NAME: self._name}}) return await self.async_step_discovery_confirm() async def async_step_discovery_confirm(self, user_input=None): """Handle user-confirmation of discovered node.""" - if user_input is not None: - return await self.async_step_credentials() + if user_input is None: + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders={"name": self._name}, + ) - return self.async_show_form( - step_id="discovery_confirm", description_placeholders={"name": self._name} - ) + return self._create_entry() async def async_step_user(self, user_input=None): """Handle the initial step.""" - return await self.async_step_host(user_input) - - async def async_step_host(self, user_input=None, errors=None): - """Handle host name and port input.""" - if not errors: - errors = {} + errors = {} if user_input is not None: self._host = user_input[CONF_HOST] self._port = user_input[CONF_PORT] self._ssl = user_input[CONF_SSL] - return await self.async_step_credentials() - return self.async_show_form( - step_id="host", data_schema=self._host_schema(), errors=errors - ) - - async def async_step_credentials(self, user_input=None): - """Handle username and password input.""" - errors = {} - if user_input is not None: - self._username = user_input.get(CONF_USERNAME) - self._password = user_input.get(CONF_PASSWORD) try: await validate_http(self.hass, self._get_data()) - return await self.async_step_ws_port() - except InvalidAuth: - errors["base"] = "invalid_auth" - except CannotConnect: - if self._discovery_name: - return self.async_abort(reason="cannot_connect") - return await self.async_step_host(errors={"base": "cannot_connect"}) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - return self.async_show_form( - step_id="credentials", data_schema=self._credentials_schema(), errors=errors - ) - - async def async_step_ws_port(self, user_input=None): - """Handle websocket port of discovered node.""" - errors = {} - if user_input is not None: - self._ws_port = user_input.get(CONF_WS_PORT) - try: await validate_ws(self.hass, self._get_data()) - return self._create_entry() + except InvalidAuth: + return await self.async_step_credentials() + except WSCannotConnect: + return await self.async_step_ws_port() except CannotConnect: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" + else: + return self._create_entry() - return self.async_show_form( - step_id="ws_port", data_schema=self._ws_port_schema(), errors=errors - ) + return self._show_user_form(errors) + + async def async_step_credentials(self, user_input=None): + """Handle username and password input.""" + errors = {} + + if user_input is not None: + self._username = user_input.get(CONF_USERNAME) + self._password = user_input.get(CONF_PASSWORD) + + try: + await validate_http(self.hass, self._get_data()) + await validate_ws(self.hass, self._get_data()) + except InvalidAuth: + errors["base"] = "invalid_auth" + except WSCannotConnect: + return await self.async_step_ws_port() + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self._create_entry() + + return self._show_credentials_form(errors) + + async def async_step_ws_port(self, user_input=None): + """Handle websocket port of discovered node.""" + errors = {} + + if user_input is not None: + self._ws_port = user_input.get(CONF_WS_PORT) + + try: + await validate_ws(self.hass, self._get_data()) + except WSCannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self._create_entry() + + return self._show_ws_port_form(errors) async def async_step_import(self, data): """Handle import from YAML.""" - # We assume that the imported values work and just create the entry - return self.async_create_entry(title=data[CONF_NAME], data=data) + reason = None + try: + await validate_http(self.hass, data) + await validate_ws(self.hass, data) + except InvalidAuth: + _LOGGER.exception("Invalid Kodi credentials") + reason = "invalid_auth" + except CannotConnect: + _LOGGER.exception("Cannot connect to Kodi") + reason = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + reason = "unknown" + else: + return self.async_create_entry(title=data[CONF_NAME], data=data) + + return self.async_abort(reason=reason) + + @callback + def _show_credentials_form(self, errors=None): + schema = vol.Schema( + { + vol.Optional( + CONF_USERNAME, description={"suggested_value": self._username} + ): str, + vol.Optional( + CONF_PASSWORD, description={"suggested_value": self._password} + ): str, + } + ) + + return self.async_show_form( + step_id="credentials", data_schema=schema, errors=errors or {} + ) + + @callback + def _show_user_form(self, errors=None): + default_port = self._port or DEFAULT_PORT + default_ssl = self._ssl or DEFAULT_SSL + schema = vol.Schema( + { + vol.Required(CONF_HOST, default=self._host): str, + vol.Required(CONF_PORT, default=default_port): int, + vol.Required(CONF_SSL, default=default_ssl): bool, + } + ) + + return self.async_show_form( + step_id="user", data_schema=schema, errors=errors or {} + ) + + @callback + def _show_ws_port_form(self, errors=None): + suggestion = self._ws_port or DEFAULT_WS_PORT + schema = vol.Schema( + { + vol.Optional( + CONF_WS_PORT, description={"suggested_value": suggestion} + ): int + } + ) + + return self.async_show_form( + step_id="ws_port", data_schema=schema, errors=errors or {} + ) @callback def _create_entry(self): @@ -215,42 +302,6 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return data - @callback - def _ws_port_schema(self): - suggestion = self._ws_port or DEFAULT_WS_PORT - return vol.Schema( - { - vol.Optional( - CONF_WS_PORT, description={"suggested_value": suggestion} - ): int - } - ) - - @callback - def _host_schema(self): - default_port = self._port or DEFAULT_PORT - default_ssl = self._ssl or DEFAULT_SSL - return vol.Schema( - { - vol.Required(CONF_HOST, default=self._host): str, - vol.Required(CONF_PORT, default=default_port): int, - vol.Required(CONF_SSL, default=default_ssl): bool, - } - ) - - @callback - def _credentials_schema(self): - return vol.Schema( - { - vol.Optional( - CONF_USERNAME, description={"suggested_value": self._username} - ): str, - vol.Optional( - CONF_PASSWORD, description={"suggested_value": self._password} - ): str, - } - ) - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" @@ -258,3 +309,7 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class WSCannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect to websocket.""" diff --git a/homeassistant/components/kodi/strings.json b/homeassistant/components/kodi/strings.json index fe7c0d52149..56a637e54ce 100644 --- a/homeassistant/components/kodi/strings.json +++ b/homeassistant/components/kodi/strings.json @@ -2,7 +2,7 @@ "config": { "flow_title": "Kodi: {name}", "step": { - "host": { + "user": { "description": "Kodi connection information. Please make sure to enable \"Allow control of Kodi via HTTP\" in System/Settings/Network/Services.", "data": { "host": "[%key:common::config_flow::data::host%]", @@ -35,7 +35,9 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "Cannot connect to discovered Kodi" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } }, "device_automation": { @@ -44,4 +46,4 @@ "turn_off": "{entity_name} was requested to turn off" } } -} \ No newline at end of file +} diff --git a/tests/components/kodi/__init__.py b/tests/components/kodi/__init__.py index bbb7c962143..31c9dff14ac 100644 --- a/tests/components/kodi/__init__.py +++ b/tests/components/kodi/__init__.py @@ -27,6 +27,8 @@ async def init_integration(hass) -> MockConfigEntry: CONF_SSL: False, } entry = MockConfigEntry(domain=DOMAIN, data=entry_data, title="name") + entry.add_to_hass(hass) + with patch("homeassistant.components.kodi.Kodi.ping", return_value=True), patch( "homeassistant.components.kodi.Kodi.get_application_properties", return_value={"version": {"major": 1, "minor": 1}}, @@ -34,7 +36,6 @@ async def init_integration(hass) -> MockConfigEntry: "homeassistant.components.kodi.get_kodi_connection", return_value=MockConnection(), ): - entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index 7933081b70f..6ea2ba9d283 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -16,9 +16,11 @@ from .util import ( TEST_WS_PORT, UUID, MockConnection, + MockWSConnection, + get_kodi_connection, ) -from tests.async_mock import AsyncMock, patch +from tests.async_mock import AsyncMock, PropertyMock, patch from tests.common import MockConfigEntry @@ -30,32 +32,55 @@ async def user_flow(hass): ) assert result["type"] == "form" assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_HOST - ) - assert result["type"] == "form" - assert result["errors"] == {} - - return result["flow_id"] - - -@pytest.fixture -async def discovery_flow(hass): - """Return a discovery flow after confirmation.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY - ) - assert result["type"] == "form" - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert result["type"] == "form" - assert result["errors"] == {} return result["flow_id"] async def test_user_flow(hass, user_flow): """Test a successful user initiated flow.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), patch( + "homeassistant.components.kodi.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kodi.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_HOST["host"] + assert result["data"] == { + **TEST_HOST, + **TEST_WS_PORT, + "password": None, + "username": None, + "name": None, + "timeout": DEFAULT_TIMEOUT, + } + + 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_valid_auth(hass, user_flow): + """Test we handle valid auth.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=InvalidAuthError, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) + + assert result["type"] == "form" + assert result["step_id"] == "credentials" + assert result["errors"] == {} + with patch( "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, ), patch( @@ -67,22 +92,61 @@ async def test_user_flow(hass, user_flow): "homeassistant.components.kodi.async_setup_entry", return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( - user_flow, TEST_CREDENTIALS + result["flow_id"], TEST_CREDENTIALS ) - assert result["type"] == "form" - assert result["errors"] == {} + assert result["type"] == "create_entry" + assert result["title"] == TEST_HOST["host"] + assert result["data"] == { + **TEST_HOST, + **TEST_WS_PORT, + **TEST_CREDENTIALS, + "name": None, + "timeout": DEFAULT_TIMEOUT, + } - result2 = await hass.config_entries.flow.async_configure( + 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_valid_ws_port(hass, user_flow): + """Test we handle valid websocket port.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch.object( + MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ): + result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) + + assert result["type"] == "form" + assert result["step_id"] == "ws_port" + assert result["errors"] == {} + + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), patch( + "homeassistant.components.kodi.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.kodi.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_WS_PORT ) - assert result2["type"] == "create_entry" - assert result2["title"] == TEST_HOST["host"] - assert result2["data"] == { + assert result["type"] == "create_entry" + assert result["title"] == TEST_HOST["host"] + assert result["data"] == { **TEST_HOST, - **TEST_CREDENTIALS, **TEST_WS_PORT, + "password": None, + "username": None, "name": None, "timeout": DEFAULT_TIMEOUT, } @@ -94,6 +158,19 @@ async def test_user_flow(hass, user_flow): async def test_form_invalid_auth(hass, user_flow): """Test we handle invalid auth.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=InvalidAuthError, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) + + assert result["type"] == "form" + assert result["step_id"] == "credentials" + assert result["errors"] == {} + with patch( "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=InvalidAuthError, @@ -102,12 +179,58 @@ async def test_form_invalid_auth(hass, user_flow): return_value=MockConnection(), ): result = await hass.config_entries.flow.async_configure( - user_flow, TEST_CREDENTIALS + result["flow_id"], TEST_CREDENTIALS ) assert result["type"] == "form" + assert result["step_id"] == "credentials" assert result["errors"] == {"base": "invalid_auth"} + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=CannotConnectError, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_CREDENTIALS + ) + + assert result["type"] == "form" + assert result["step_id"] == "credentials" + assert result["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=Exception, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_CREDENTIALS + ) + + assert result["type"] == "form" + assert result["step_id"] == "credentials" + assert result["errors"] == {"base": "unknown"} + + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch.object( + MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_CREDENTIALS + ) + + assert result["type"] == "form" + assert result["step_id"] == "ws_port" + assert result["errors"] == {} + async def test_form_cannot_connect_http(hass, user_flow): """Test we handle cannot connect over HTTP error.""" @@ -118,11 +241,10 @@ async def test_form_cannot_connect_http(hass, user_flow): "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ): - result = await hass.config_entries.flow.async_configure( - user_flow, TEST_CREDENTIALS - ) + result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) assert result["type"] == "form" + assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -134,11 +256,10 @@ async def test_form_exception_http(hass, user_flow): "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ): - result = await hass.config_entries.flow.async_configure( - user_flow, TEST_CREDENTIALS - ) + result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) assert result["type"] == "form" + assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -146,108 +267,114 @@ async def test_form_cannot_connect_ws(hass, user_flow): """Test we handle cannot connect over WebSocket error.""" with patch( "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch.object( + MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + new=get_kodi_connection, + ): + result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) + + assert result["type"] == "form" + assert result["step_id"] == "ws_port" + assert result["errors"] == {} + + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch.object( + MockWSConnection, "connected", new_callable=PropertyMock(return_value=False) + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, ): result = await hass.config_entries.flow.async_configure( - user_flow, TEST_CREDENTIALS - ) - - with patch.object( - MockConnection, "connect", AsyncMock(side_effect=CannotConnectError) - ), patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), - ): - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_WS_PORT ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} - - with patch( - "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(connected=False), - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], TEST_WS_PORT - ) - - assert result3["type"] == "form" - assert result3["errors"] == {"base": "cannot_connect"} + assert result["type"] == "form" + assert result["step_id"] == "ws_port" + assert result["errors"] == {"base": "cannot_connect"} with patch( "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=CannotConnectError, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + new=get_kodi_connection, ): - result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], TEST_WS_PORT + result = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_WS_PORT ) - assert result4["type"] == "form" - assert result4["errors"] == {"base": "cannot_connect"} + assert result["type"] == "form" + assert result["step_id"] == "ws_port" + assert result["errors"] == {"base": "cannot_connect"} async def test_form_exception_ws(hass, user_flow): """Test we handle generic exception over WebSocket.""" with patch( "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch.object( + MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + new=get_kodi_connection, ): - result = await hass.config_entries.flow.async_configure( - user_flow, TEST_CREDENTIALS - ) + result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) + + assert result["type"] == "form" + assert result["step_id"] == "ws_port" + assert result["errors"] == {} with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=Exception, + "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch.object( + MockWSConnection, "connect", AsyncMock(side_effect=Exception) ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", - return_value=MockConnection(), + new=get_kodi_connection, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_WS_PORT ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == "form" + assert result["step_id"] == "ws_port" + assert result["errors"] == {"base": "unknown"} -async def test_discovery(hass, discovery_flow): +async def test_discovery(hass): """Test discovery flow works.""" with patch( "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), - ), patch( + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + ) + + assert result["type"] == "form" + assert result["step_id"] == "discovery_confirm" + + with patch( "homeassistant.components.kodi.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.kodi.async_setup_entry", return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( - discovery_flow, TEST_CREDENTIALS + flow_id=result["flow_id"], user_input={} ) - assert result["type"] == "form" - assert result["errors"] == {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_WS_PORT - ) - - assert result2["type"] == "create_entry" - assert result2["title"] == "hostname" - assert result2["data"] == { + assert result["type"] == "create_entry" + assert result["title"] == "hostname" + assert result["data"] == { **TEST_HOST, - **TEST_CREDENTIALS, **TEST_WS_PORT, + "password": None, + "username": None, "name": "hostname", "timeout": DEFAULT_TIMEOUT, } @@ -257,7 +384,7 @@ async def test_discovery(hass, discovery_flow): assert len(mock_setup_entry.mock_calls) == 1 -async def test_discovery_cannot_connect_http(hass, discovery_flow): +async def test_discovery_cannot_connect_http(hass): """Test discovery aborts if cannot connect.""" with patch( "homeassistant.components.kodi.config_flow.Kodi.ping", @@ -266,19 +393,86 @@ async def test_discovery_cannot_connect_http(hass, discovery_flow): "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ): - result = await hass.config_entries.flow.async_configure( - discovery_flow, TEST_CREDENTIALS + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY ) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" -async def test_discovery_duplicate_data(hass, discovery_flow): +async def test_discovery_cannot_connect_ws(hass): + """Test discovery aborts if cannot connect to websocket.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch.object( + MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + new=get_kodi_connection, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + ) + + assert result["type"] == "form" + assert result["step_id"] == "ws_port" + assert result["errors"] == {} + + +async def test_discovery_exception_http(hass, user_flow): + """Test we handle generic exception during discovery validation.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=Exception, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + ) + + assert result["type"] == "abort" + assert result["reason"] == "unknown" + + +async def test_discovery_invalid_auth(hass): + """Test we handle invalid auth during discovery.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=InvalidAuthError, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + ) + + assert result["type"] == "form" + assert result["step_id"] == "credentials" + assert result["errors"] == {} + + +async def test_discovery_duplicate_data(hass): """Test discovery aborts if same mDNS packet arrives.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY + ) + + assert result["type"] == "form" + assert result["step_id"] == "discovery_confirm" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY ) + assert result["type"] == "abort" assert result["reason"] == "already_in_progress" @@ -308,6 +502,11 @@ async def test_discovery_updates_unique_id(hass): async def test_form_import(hass): """Test we get the form with import source.""" with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ), patch( "homeassistant.components.kodi.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.kodi.async_setup_entry", return_value=True, @@ -323,3 +522,53 @@ async def test_form_import(hass): 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_import_invalid_auth(hass): + """Test we handle invalid auth on import.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=InvalidAuthError, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_IMPORT, + ) + + assert result["type"] == "abort" + assert result["reason"] == "invalid_auth" + + +async def test_form_import_cannot_connect(hass): + """Test we handle cannot connect on import.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=CannotConnectError, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_IMPORT, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +async def test_form_import_exception(hass): + """Test we handle unknown exception on import.""" + with patch( + "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=Exception, + ), patch( + "homeassistant.components.kodi.config_flow.get_kodi_connection", + return_value=MockConnection(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_IMPORT, + ) + + assert result["type"] == "abort" + assert result["reason"] == "unknown" diff --git a/tests/components/kodi/util.py b/tests/components/kodi/util.py index 0e0582047d1..5a47ea88631 100644 --- a/tests/components/kodi/util.py +++ b/tests/components/kodi/util.py @@ -36,6 +36,16 @@ TEST_IMPORT = { } +def get_kodi_connection( + host, port, ws_port, username, password, ssl=False, timeout=5, session=None +): + """Get Kodi connection.""" + if ws_port is None: + return MockConnection() + else: + return MockWSConnection() + + class MockConnection: """A mock kodi connection.""" @@ -65,3 +75,34 @@ class MockConnection: def server(self): """Mock server.""" return None + + +class MockWSConnection: + """A mock kodi websocket connection.""" + + def __init__(self, connected=True): + """Mock the websocket connection.""" + self._connected = connected + + async def connect(self): + """Mock connect.""" + pass + + @property + def connected(self): + """Mock connected.""" + return self._connected + + @property + def can_subscribe(self): + """Mock can_subscribe.""" + return False + + async def close(self): + """Mock close.""" + pass + + @property + def server(self): + """Mock server.""" + return None From 41ba1dff71156925c03187b9d719628b1ff48990 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 22 Aug 2020 20:48:25 +0200 Subject: [PATCH 280/862] Upgrade beautifulsoup4 to 4.9.1 (#39158) --- homeassistant/components/scrape/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 7a76512f01d..dcb3a8fdff0 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -2,7 +2,7 @@ "domain": "scrape", "name": "Scrape", "documentation": "https://www.home-assistant.io/integrations/scrape", - "requirements": ["beautifulsoup4==4.9.0"], + "requirements": ["beautifulsoup4==4.9.1"], "after_dependencies": ["rest"], "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index 70c69aba98c..425f33c8e32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -328,7 +328,7 @@ batinfo==0.4.2 # beacontools[scan]==1.2.3 # homeassistant.components.scrape -beautifulsoup4==4.9.0 +beautifulsoup4==4.9.1 # homeassistant.components.beewi_smartclim beewi_smartclim==0.0.7 From 644e826ca7ff3c9284610a531d29c0892aab48c4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 22 Aug 2020 20:53:01 +0200 Subject: [PATCH 281/862] Upgrade praw to 7.1.0 (#39152) --- homeassistant/components/reddit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 189ffbbc4be..fc3356b310c 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,6 +2,6 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": ["praw==6.5.1"], + "requirements": ["praw==7.1.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 425f33c8e32..816e4967279 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1109,7 +1109,7 @@ pocketcasts==0.1 poolsense==0.0.8 # homeassistant.components.reddit -praw==6.5.1 +praw==7.1.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0c8786ebd6..d7844c6532a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -529,7 +529,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==6.5.1 +praw==7.1.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 From 1f5b948ead69f9724b86a944bc88ca77084dd70b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 22 Aug 2020 20:57:28 +0200 Subject: [PATCH 282/862] Upgrade connect-box to 0.2.7 (#39162) --- CODEOWNERS | 2 +- homeassistant/components/upc_connect/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 70007e16539..de2fae6e532 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -447,7 +447,7 @@ homeassistant/components/ubee/* @mzdrale homeassistant/components/unifi/* @Kane610 homeassistant/components/unifiled/* @florisvdk homeassistant/components/upb/* @gwww -homeassistant/components/upc_connect/* @pvizeli +homeassistant/components/upc_connect/* @pvizeli @fabaff homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core homeassistant/components/upnp/* @StevenLooman diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 6236021f3c6..ebdcc630820 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,6 +2,6 @@ "domain": "upc_connect", "name": "UPC Connect Box", "documentation": "https://www.home-assistant.io/integrations/upc_connect", - "requirements": ["connect-box==0.2.5"], - "codeowners": ["@pvizeli"] + "requirements": ["connect-box==0.2.7"], + "codeowners": ["@pvizeli", "@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index 816e4967279..f8cf6a92d47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -435,7 +435,7 @@ colorlog==4.1.0 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.5 +connect-box==0.2.7 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From a1845e9ef082ed0f0f7774b92a43f8b3995715b2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 22 Aug 2020 21:02:12 +0200 Subject: [PATCH 283/862] Upgrade sendgrid to 6.4.6 (#39148) --- homeassistant/components/sendgrid/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index 52717c55124..d09ec684e52 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -2,6 +2,6 @@ "domain": "sendgrid", "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", - "requirements": ["sendgrid==6.2.1"], + "requirements": ["sendgrid==6.4.6"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index f8cf6a92d47..51885e17ce3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1952,7 +1952,7 @@ schiene==0.23 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.2.1 +sendgrid==6.4.6 # homeassistant.components.sensehat sense-hat==2.2.0 From 7ca8e9077a5dac3e91866bb1fff4e3c3546ddbae Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 22 Aug 2020 21:09:00 +0200 Subject: [PATCH 284/862] Upgrade python-whois to 0.7.3 (#39153) --- homeassistant/components/whois/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index 4330604f9bd..39cc1c194c8 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -2,6 +2,6 @@ "domain": "whois", "name": "Whois", "documentation": "https://www.home-assistant.io/integrations/whois", - "requirements": ["python-whois==0.7.2"], + "requirements": ["python-whois==0.7.3"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 51885e17ce3..a391de93a74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1783,7 +1783,7 @@ python-velbus==2.0.44 python-vlc==1.1.2 # homeassistant.components.whois -python-whois==0.7.2 +python-whois==0.7.3 # homeassistant.components.wink python-wink==1.10.5 From 69de68c025f2ea4f337f04f15331c4aa7b736328 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 22 Aug 2020 22:31:39 +0200 Subject: [PATCH 285/862] Upgrade slixmpp to 1.5.2 (#39169) --- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index a82e3492599..2453ce61b05 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,6 +2,6 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.5.1"], + "requirements": ["slixmpp==1.5.2"], "codeowners": ["@fabaff", "@flowolf"] } diff --git a/requirements_all.txt b/requirements_all.txt index a391de93a74..04a5b01bbf6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1991,7 +1991,7 @@ slackclient==2.5.0 sleepyq==0.7 # homeassistant.components.xmpp -slixmpp==1.5.1 +slixmpp==1.5.2 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.0 From fa09a93cfe13fb16613fca7676215e315f1a7680 Mon Sep 17 00:00:00 2001 From: ktownsend-personal Date: Sat, 22 Aug 2020 16:19:20 -0500 Subject: [PATCH 286/862] add zone status attribute so we can know if queued (#39133) Before this proposed change, all of the zones related to a running program turn "on", but it's not possible to know which zone is actually running (vs. queued). Adding the mapped state values (they are the same as program status values) as an attribute will allow inspection of all 3 states. --- homeassistant/components/rainmachine/switch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 74704398061..c0dc450ee2b 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -45,7 +45,7 @@ ATTR_ZONES = "zones" DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] -PROGRAM_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"} +RUN_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"} SOIL_TYPE_MAP = { 0: "Not Set", @@ -233,7 +233,7 @@ class RainMachineProgram(RainMachineSwitch): ATTR_ID: self._switch_data["uid"], ATTR_NEXT_RUN: next_run, ATTR_SOAK: self._switch_data.get("soak"), - ATTR_STATUS: PROGRAM_STATUS_MAP[self._switch_data["status"]], + ATTR_STATUS: RUN_STATUS_MAP[self._switch_data["status"]], ATTR_ZONES: ", ".join(z["name"] for z in self.zones), } ) @@ -290,6 +290,7 @@ class RainMachineZone(RainMachineSwitch): self._attrs.update( { + ATTR_STATUS: RUN_STATUS_MAP[self._switch_data["state"]], ATTR_AREA: details.get("waterSense").get("area"), ATTR_CURRENT_CYCLE: self._switch_data.get("cycle"), ATTR_FIELD_CAPACITY: details.get("waterSense").get("fieldCapacity"), From bd136fa79ac74d18b9d7b8b09628c96b136d4712 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Aug 2020 01:09:36 +0200 Subject: [PATCH 287/862] Upgrade sqlalchemy to 1.3.19 (#39167) --- 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 6a3933541f4..6983dfbd690 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.18"], + "requirements": ["sqlalchemy==1.3.19"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index bdb1c0582b5..c79f29d6eed 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.18"], + "requirements": ["sqlalchemy==1.3.19"], "codeowners": ["@dgomes"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index aa776cb4864..417d7654026 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ pytz>=2020.1 pyyaml==5.3.1 requests==2.24.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.18 +sqlalchemy==1.3.19 voluptuous-serialize==2.4.0 voluptuous==0.11.7 yarl==1.4.2 diff --git a/requirements_all.txt b/requirements_all.txt index 04a5b01bbf6..5ae0e01b94f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2051,7 +2051,7 @@ spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.18 +sqlalchemy==1.3.19 # homeassistant.components.starline starline==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7844c6532a..6810faae9e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -937,7 +937,7 @@ spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.18 +sqlalchemy==1.3.19 # homeassistant.components.starline starline==0.1.3 From 2beca3e69a810f0b8d89997dabaca5cf8aa7a98f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Aug 2020 01:11:24 +0200 Subject: [PATCH 288/862] Upgrade jinja2 to >=2.11.2 (#39161) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 417d7654026..b01126b9574 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ emoji==0.5.4 hass-nabucasa==0.35.0 home-assistant-frontend==20200820.0 importlib-metadata==1.6.0;python_version<'3.8' -jinja2>=2.11.1 +jinja2>=2.11.2 netdisco==2.8.2 paho-mqtt==1.5.0 pillow==7.2.0 diff --git a/requirements.txt b/requirements.txt index 702e4eaf19f..b7da4348d02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ bcrypt==3.1.7 certifi>=2020.4.5.1 ciso8601==2.1.3 importlib-metadata==1.6.0;python_version<'3.8' -jinja2>=2.11.1 +jinja2>=2.11.2 PyJWT==1.7.1 cryptography==2.9.2 pip>=8.0.3 diff --git a/setup.py b/setup.py index 81f8727ed60..bb57c4fdf7b 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ REQUIRES = [ "certifi>=2020.4.5.1", "ciso8601==2.1.3", "importlib-metadata==1.6.0;python_version<'3.8'", - "jinja2>=2.11.1", + "jinja2>=2.11.2", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==2.9.2", From 9baf3ff706d1bdb192dcaf993fe8806e6570f6a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2020 18:12:53 -0500 Subject: [PATCH 289/862] Update universal media_player to use async_track_template_result (#39054) * Update universal media_player to use async_track_template_result * Review comments and add missing test cover --- .../components/universal/media_player.py | 39 ++++-- .../components/universal/test_media_player.py | 113 +++++++++++++++--- 2 files changed, 124 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 289a57683cf..bff5ad1542b 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -68,7 +68,8 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import callback +from homeassistant.core import EVENT_HOMEASSISTANT_START, callback +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_call_from_config @@ -132,27 +133,45 @@ class UniversalMediaPlayer(MediaPlayerEntity): attr.append(None) self._attrs[key] = attr self._child_state = None + self._state_template_result = None self._state_template = state_template - if state_template is not None: - self._state_template.hass = hass async def async_added_to_hass(self): """Subscribe to children and template state changes.""" @callback - def async_on_dependency_update(*_): + def _async_on_dependency_update(*_): """Update ha state when dependencies update.""" self.async_schedule_update_ha_state(True) + @callback + def _async_on_template_update(event, template, last_result, result): + """Update ha state when dependencies update.""" + if isinstance(result, TemplateError): + self._state_template_result = None + else: + self._state_template_result = result + self.async_schedule_update_ha_state(True) + + if self._state_template is not None: + result = self.hass.helpers.event.async_track_template_result( + self._state_template, _async_on_template_update + ) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, callback(lambda _: result.async_refresh()) + ) + + self.async_on_remove(result.async_remove) + depend = copy(self._children) for entity in self._attrs.values(): depend.append(entity[0]) - if self._state_template is not None: - for entity in self._state_template.extract_entities(): - depend.append(entity) - self.hass.helpers.event.async_track_state_change_event( - list(set(depend)), async_on_dependency_update + self.async_on_remove( + self.hass.helpers.event.async_track_state_change_event( + list(set(depend)), _async_on_dependency_update + ) ) def _entity_lkp(self, entity_id, state_attr=None): @@ -217,7 +236,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): def master_state(self): """Return the master state for entity or None.""" if self._state_template is not None: - return self._state_template.async_render() + return self._state_template_result if CONF_STATE in self._attrs: master_state = self._entity_lkp( self._attrs[CONF_STATE][0], self._attrs[CONF_STATE][1] diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index b50906649f0..af2132e8f69 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -10,7 +10,14 @@ import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player import homeassistant.components.switch as switch import homeassistant.components.universal.media_player as universal -from homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import ( + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, + STATE_UNKNOWN, +) +from homeassistant.setup import async_setup_component from tests.common import get_test_home_assistant, mock_service @@ -337,23 +344,6 @@ class TestMediaPlayer(unittest.TestCase): self.hass.states.set(self.mock_state_switch_id, STATE_ON) assert STATE_ON == ump.master_state - def test_master_state_with_template(self): - """Test the state_template option.""" - config = copy(self.config_children_and_attr) - self.hass.states.set("input_boolean.test", STATE_OFF) - templ = ( - '{% if states.input_boolean.test.state == "off" %}on' - "{% else %}{{ states.media_player.mock1.state }}{% endif %}" - ) - config["state_template"] = templ - config = validate_config(config) - - ump = universal.UniversalMediaPlayer(self.hass, **config) - - assert STATE_ON == ump.master_state - self.hass.states.set("input_boolean.test", STATE_ON) - assert STATE_OFF == ump.master_state - def test_master_state_with_bad_attrs(self): """Test master state property.""" config = copy(self.config_children_and_attr) @@ -735,3 +725,90 @@ class TestMediaPlayer(unittest.TestCase): asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 1 == len(service) + + +async def test_state_template(hass): + """Test with a simple valid state template.""" + hass.states.async_set("sensor.test_sensor", STATE_ON) + + await async_setup_component( + hass, + "media_player", + { + "media_player": { + "platform": "universal", + "name": "tv", + "state_template": "{{ states.sensor.test_sensor.state }}", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + await hass.async_start() + + await hass.async_block_till_done() + assert hass.states.get("media_player.tv").state == STATE_ON + hass.states.async_set("sensor.test_sensor", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("media_player.tv").state == STATE_OFF + + +async def test_invalid_state_template(hass): + """Test invalid state template sets state to None.""" + hass.states.async_set("sensor.test_sensor", "on") + + await async_setup_component( + hass, + "media_player", + { + "media_player": { + "platform": "universal", + "name": "tv", + "state_template": "{{ states.sensor.test_sensor.state + x }}", + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + await hass.async_start() + + await hass.async_block_till_done() + assert hass.states.get("media_player.tv").state == STATE_UNKNOWN + hass.states.async_set("sensor.test_sensor", "off") + await hass.async_block_till_done() + assert hass.states.get("media_player.tv").state == STATE_UNKNOWN + + +async def test_master_state_with_template(hass): + """Test the state_template option.""" + hass.states.async_set("input_boolean.test", STATE_OFF) + hass.states.async_set("media_player.mock1", STATE_OFF) + + templ = ( + '{% if states.input_boolean.test.state == "off" %}on' + "{% else %}{{ states.media_player.mock1.state }}{% endif %}" + ) + + await async_setup_component( + hass, + "media_player", + { + "media_player": { + "platform": "universal", + "name": "tv", + "state_template": templ, + } + }, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + await hass.async_start() + + await hass.async_block_till_done() + hass.states.get("media_player.tv").state == STATE_ON + + hass.states.async_set("input_boolean.test", STATE_ON) + await hass.async_block_till_done() + + hass.states.get("media_player.tv").state == STATE_OFF From 73328dab5e43fb7f8436ab85e5e1c932cb390a64 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Aug 2020 01:18:05 +0200 Subject: [PATCH 290/862] Upgrade psutil to 5.7.2 (#39149) --- homeassistant/components/systemmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 5753dbbf682..3b08a7afe14 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -2,6 +2,6 @@ "domain": "systemmonitor", "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": ["psutil==5.7.0"], + "requirements": ["psutil==5.7.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 5ae0e01b94f..f9bdd548fdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1127,7 +1127,7 @@ protobuf==3.12.2 proxmoxer==1.1.1 # homeassistant.components.systemmonitor -psutil==5.7.0 +psutil==5.7.2 # homeassistant.components.ptvsd ptvsd==4.3.2 From 3198233b8f38010996df09bd7fe63f85bdb2adf5 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sun, 23 Aug 2020 02:30:26 +0300 Subject: [PATCH 291/862] Add binary sensors to Risco integration (#39137) * Add binary sensors to Risco integration * Minor cleanups * RiscoEntity base class * Platinum score * Remove alarm parameter in _setup_risco * Avoid zones and partitions sharing unique ids --- homeassistant/components/risco/__init__.py | 2 +- .../components/risco/alarm_control_panel.py | 36 +--- .../components/risco/binary_sensor.py | 66 ++++++++ homeassistant/components/risco/entity.py | 45 +++++ homeassistant/components/risco/manifest.json | 3 +- .../risco/test_alarm_control_panel.py | 8 +- tests/components/risco/test_binary_sensor.py | 156 ++++++++++++++++++ 7 files changed, 278 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/risco/binary_sensor.py create mode 100644 homeassistant/components/risco/entity.py create mode 100644 tests/components/risco/test_binary_sensor.py diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index e3e59229bf5..6ee126145b3 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DATA_COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN -PLATFORMS = ["alarm_control_panel"] +PLATFORMS = ["alarm_control_panel", "binary_sensor"] UNDO_UPDATE_LISTENER = "undo_update_listener" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 096c4023ffe..b0786c77c79 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ) from .const import DATA_COORDINATOR, DOMAIN +from .entity import RiscoEntity _LOGGER = logging.getLogger(__name__) @@ -37,39 +38,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, False) -class RiscoAlarm(AlarmControlPanelEntity): +class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): """Representation of a Risco partition.""" def __init__(self, coordinator, partition_id): """Init the partition.""" - self._coordinator = coordinator + super().__init__(coordinator) self._partition_id = partition_id self._partition = self._coordinator.data.partitions[self._partition_id] - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self._coordinator.last_update_success - - def _refresh_from_coordinator(self): + def _get_data_from_coordinator(self): self._partition = self._coordinator.data.partitions[self._partition_id] - self.async_write_ha_state() - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._coordinator.async_add_listener(self._refresh_from_coordinator) - ) - - @property - def _risco(self): - """Return the Risco API object.""" - return self._coordinator.risco @property def device_info(self): @@ -132,10 +111,3 @@ class RiscoAlarm(AlarmControlPanelEntity): alarm = await getattr(self._risco, method)(self._partition_id) self._partition = alarm.partitions[self._partition_id] self.async_write_ha_state() - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py new file mode 100644 index 00000000000..978e6d11eb6 --- /dev/null +++ b/homeassistant/components/risco/binary_sensor.py @@ -0,0 +1,66 @@ +"""Support for Risco alarm zones.""" +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + BinarySensorEntity, +) + +from .const import DATA_COORDINATOR, DOMAIN +from .entity import RiscoEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Risco alarm control panel.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + entities = [ + RiscoBinarySensor(coordinator, zone_id, zone) + for zone_id, zone in coordinator.data.zones.items() + ] + + async_add_entities(entities, False) + + +class RiscoBinarySensor(BinarySensorEntity, RiscoEntity): + """Representation of a Risco zone as a binary sensor.""" + + def __init__(self, coordinator, zone_id, zone): + """Init the zone.""" + super().__init__(coordinator) + self._zone_id = zone_id + self._zone = zone + + def _get_data_from_coordinator(self): + self._zone = self._coordinator.data.zones[self._zone_id] + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Risco", + } + + @property + def name(self): + """Return the name of the zone.""" + return self._zone.name + + @property + def unique_id(self): + """Return a unique id for this zone.""" + return f"{self._risco.site_uuid}_zone_{self._zone_id}" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {"bypassed": self._zone.bypassed} + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._zone.triggered + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + return DEVICE_CLASS_MOTION diff --git a/homeassistant/components/risco/entity.py b/homeassistant/components/risco/entity.py new file mode 100644 index 00000000000..0c74cdf8264 --- /dev/null +++ b/homeassistant/components/risco/entity.py @@ -0,0 +1,45 @@ +"""A risco entity base class.""" +from homeassistant.helpers.entity import Entity + + +class RiscoEntity(Entity): + """Risco entity base class.""" + + def __init__(self, coordinator): + """Init the instance.""" + self._coordinator = coordinator + + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @property + def available(self): + """Return if entity is available.""" + return self._coordinator.last_update_success + + def _get_data_from_coordinator(self): + raise NotImplementedError + + def _refresh_from_coordinator(self): + self._get_data_from_coordinator() + self.async_write_ha_state() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self._refresh_from_coordinator) + ) + + @property + def _risco(self): + """Return the Risco API object.""" + return self._coordinator.risco + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 9dd6fd95680..af8bdc960f2 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -8,5 +8,6 @@ ], "codeowners": [ "@OnFreund" - ] + ], + "quality_scale": "platinum" } \ No newline at end of file diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 0219ceca7bd..46e285199ad 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -63,7 +63,7 @@ def two_part_alarm(): yield alarm_mock -async def _setup_risco(hass, alarm=MagicMock()): +async def _setup_risco(hass): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) config_entry.add_to_hass(hass) @@ -121,7 +121,7 @@ async def test_setup(hass, two_part_alarm): assert not registry.async_is_registered(FIRST_ENTITY_ID) assert not registry.async_is_registered(SECOND_ENTITY_ID) - await _setup_risco(hass, two_part_alarm) + await _setup_risco(hass) assert registry.async_is_registered(FIRST_ENTITY_ID) assert registry.async_is_registered(SECOND_ENTITY_ID) @@ -146,7 +146,7 @@ async def _check_state(hass, alarm, property, state, entity_id, partition_id): async def test_states(hass, two_part_alarm): """Test the various alarm states.""" - await _setup_risco(hass, two_part_alarm) + await _setup_risco(hass) assert hass.states.get(FIRST_ENTITY_ID).state == STATE_UNKNOWN await _check_state( @@ -207,7 +207,7 @@ async def _call_alarm_service(hass, service, entity_id): async def test_sets(hass, two_part_alarm): """Test settings the various modes.""" - await _setup_risco(hass, two_part_alarm) + await _setup_risco(hass) await _test_servie_call(hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0) await _test_servie_call(hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1) diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py new file mode 100644 index 00000000000..46b3bae4c78 --- /dev/null +++ b/tests/components/risco/test_binary_sensor.py @@ -0,0 +1,156 @@ +"""Tests for the Risco binary sensors.""" +import pytest + +from homeassistant.components.risco import CannotConnectError, UnauthorizedError +from homeassistant.components.risco.const import DOMAIN +from homeassistant.const import ( + CONF_PASSWORD, + CONF_PIN, + CONF_USERNAME, + STATE_OFF, + STATE_ON, +) +from homeassistant.helpers.entity_component import async_update_entity + +from tests.async_mock import MagicMock, PropertyMock, patch +from tests.common import MockConfigEntry + +TEST_CONFIG = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PIN: "1234", +} +TEST_SITE_UUID = "test-site-uuid" +TEST_SITE_NAME = "test-site-name" +FIRST_ENTITY_ID = "binary_sensor.zone_0" +SECOND_ENTITY_ID = "binary_sensor.zone_1" + + +def _zone_mock(): + return MagicMock(triggered=False, bypassed=False,) + + +@pytest.fixture +def two_zone_alarm(): + """Fixture to mock alarm with two zones.""" + zone_mocks = {0: _zone_mock(), 1: _zone_mock()} + alarm_mock = MagicMock() + with patch.object( + zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), patch.object( + zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") + ), patch.object( + zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), patch.object( + zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") + ), patch.object( + alarm_mock, "zones", new_callable=PropertyMock(return_value=zone_mocks), + ), patch( + "homeassistant.components.risco.RiscoAPI.get_state", return_value=alarm_mock, + ): + yield alarm_mock + + +async def _setup_risco(hass): + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.risco.RiscoAPI.login", return_value=True, + ), patch( + "homeassistant.components.risco.RiscoAPI.site_uuid", + new_callable=PropertyMock(return_value=TEST_SITE_UUID), + ), patch( + "homeassistant.components.risco.RiscoAPI.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.RiscoAPI.close" + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +async def test_cannot_connect(hass): + """Test connection error.""" + + with patch( + "homeassistant.components.risco.RiscoAPI.login", side_effect=CannotConnectError, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + registry = await hass.helpers.entity_registry.async_get_registry() + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + +async def test_unauthorized(hass): + """Test unauthorized error.""" + + with patch( + "homeassistant.components.risco.RiscoAPI.login", side_effect=UnauthorizedError, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + registry = await hass.helpers.entity_registry.async_get_registry() + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + +async def test_setup(hass, two_zone_alarm): + """Test entity setup.""" + registry = await hass.helpers.entity_registry.async_get_registry() + + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + await _setup_risco(hass) + + assert registry.async_is_registered(FIRST_ENTITY_ID) + assert registry.async_is_registered(SECOND_ENTITY_ID) + + registry = await hass.helpers.device_registry.async_get_registry() + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0")}, {}) + assert device is not None + assert device.manufacturer == "Risco" + + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_1")}, {}) + assert device is not None + assert device.manufacturer == "Risco" + + +async def _check_state(hass, alarm, triggered, bypassed, entity_id, zone_id): + with patch.object( + alarm.zones[zone_id], + "triggered", + new_callable=PropertyMock(return_value=triggered), + ), patch.object( + alarm.zones[zone_id], + "bypassed", + new_callable=PropertyMock(return_value=bypassed), + ): + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + + expected_triggered = STATE_ON if triggered else STATE_OFF + assert hass.states.get(entity_id).state == expected_triggered + assert hass.states.get(entity_id).attributes["bypassed"] == bypassed + + +async def test_states(hass, two_zone_alarm): + """Test the various alarm states.""" + await _setup_risco(hass) + + await _check_state(hass, two_zone_alarm, True, True, FIRST_ENTITY_ID, 0) + await _check_state(hass, two_zone_alarm, True, False, FIRST_ENTITY_ID, 0) + await _check_state(hass, two_zone_alarm, False, True, FIRST_ENTITY_ID, 0) + await _check_state(hass, two_zone_alarm, False, False, FIRST_ENTITY_ID, 0) + await _check_state(hass, two_zone_alarm, True, True, SECOND_ENTITY_ID, 1) + await _check_state(hass, two_zone_alarm, True, False, SECOND_ENTITY_ID, 1) + await _check_state(hass, two_zone_alarm, False, True, SECOND_ENTITY_ID, 1) + await _check_state(hass, two_zone_alarm, False, False, SECOND_ENTITY_ID, 1) From dfa18b4b6abf47ad144756aae776a2cf5d139da9 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 22 Aug 2020 18:31:53 -0500 Subject: [PATCH 292/862] Fix unmocked calls in melcloud (#39170) * fix unmocked calls in melcloud * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py --- tests/components/melcloud/test_config_flow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index e6b36306986..ab3d16a0d6a 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -16,7 +16,9 @@ from tests.common import MockConfigEntry @pytest.fixture def mock_login(): """Mock pymelcloud login.""" - with patch("pymelcloud.login") as mock: + with patch( + "homeassistant.components.melcloud.config_flow.pymelcloud.login" + ) as mock: mock.return_value = "test-token" yield mock @@ -24,7 +26,9 @@ def mock_login(): @pytest.fixture def mock_get_devices(): """Mock pymelcloud get_devices.""" - with patch("pymelcloud.get_devices") as mock: + with patch( + "homeassistant.components.melcloud.config_flow.pymelcloud.get_devices" + ) as mock: mock.return_value = { pymelcloud.DEVICE_TYPE_ATA: [], pymelcloud.DEVICE_TYPE_ATW: [], From 3cc099af8075a5fc2726b4564c0a0f054584414e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2020 02:50:59 -0500 Subject: [PATCH 293/862] Make emulated_hue upnp responder async (#39126) --- .coveragerc | 1 - .../components/emulated_hue/__init__.py | 21 +- homeassistant/components/emulated_hue/upnp.py | 184 ++++++++---------- tests/components/emulated_hue/test_hue_api.py | 2 +- tests/components/emulated_hue/test_upnp.py | 97 ++++++--- 5 files changed, 166 insertions(+), 139 deletions(-) diff --git a/.coveragerc b/.coveragerc index 214b01c0d42..bccf6bbf2ea 100644 --- a/.coveragerc +++ b/.coveragerc @@ -220,7 +220,6 @@ omit = homeassistant/components/emby/media_player.py homeassistant/components/emoncms/sensor.py homeassistant/components/emoncms_history/* - homeassistant/components/emulated_hue/upnp.py homeassistant/components/enigma2/media_player.py homeassistant/components/enocean/__init__.py homeassistant/components/enocean/binary_sensor.py diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 13a4af544c1..5e50c727728 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -21,7 +21,7 @@ from .hue_api import ( HueUnauthorizedUser, HueUsernameView, ) -from .upnp import DescriptionXmlView, UPNPResponderThread +from .upnp import DescriptionXmlView, create_upnp_datagram_endpoint DOMAIN = "emulated_hue" @@ -120,17 +120,22 @@ async def async_setup(hass, yaml_config): HueGroupView(config).register(app, app.router) HueFullStateView(config).register(app, app.router) - upnp_listener = UPNPResponderThread( + listen = create_upnp_datagram_endpoint( config.host_ip_addr, - config.listen_port, config.upnp_bind_multicast, config.advertise_ip, - config.advertise_port, + config.advertise_port or config.listen_port, ) + protocol = None async def stop_emulated_hue_bridge(event): """Stop the emulated hue bridge.""" - upnp_listener.stop() + nonlocal protocol + nonlocal site + nonlocal runner + + if protocol: + protocol.close() if site: await site.stop() if runner: @@ -138,10 +143,12 @@ async def async_setup(hass, yaml_config): async def start_emulated_hue_bridge(event): """Start the emulated hue bridge.""" - upnp_listener.start() + nonlocal protocol nonlocal site nonlocal runner + _, protocol = await listen + runner = web.AppRunner(app) await runner.setup() @@ -153,6 +160,8 @@ async def async_setup(hass, yaml_config): _LOGGER.error( "Failed to create HTTP server at port %d: %s", config.listen_port, error ) + if protocol: + protocol.close() else: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 8adcac1a52f..bc4def242c3 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -1,8 +1,7 @@ """Support UPNP discovery method that mimics Hue hubs.""" +import asyncio import logging -import select import socket -import threading from aiohttp import web @@ -13,6 +12,9 @@ from .const import HUE_SERIAL_NUMBER, HUE_UUID _LOGGER = logging.getLogger(__name__) +BROADCAST_PORT = 1900 +BROADCAST_ADDR = "239.255.255.250" + class DescriptionXmlView(HomeAssistantView): """Handles requests for the description.xml file.""" @@ -53,106 +55,95 @@ class DescriptionXmlView(HomeAssistantView): return web.Response(text=resp_text, content_type="text/xml") -class UPNPResponderThread(threading.Thread): +@core.callback +def create_upnp_datagram_endpoint( + host_ip_addr, upnp_bind_multicast, advertise_ip, advertise_port, +): + """Create the UPNP socket and protocol.""" + + # Listen for UDP port 1900 packets sent to SSDP multicast address + ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + ssdp_socket.setblocking(False) + + # Required for receiving multicast + ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + ssdp_socket.setsockopt( + socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(host_ip_addr) + ) + + ssdp_socket.setsockopt( + socket.SOL_IP, + socket.IP_ADD_MEMBERSHIP, + socket.inet_aton(BROADCAST_ADDR) + socket.inet_aton(host_ip_addr), + ) + + ssdp_socket.bind(("" if upnp_bind_multicast else host_ip_addr, BROADCAST_PORT)) + + loop = asyncio.get_event_loop() + + return loop.create_datagram_endpoint( + lambda: UPNPResponderProtocol(loop, ssdp_socket, advertise_ip, advertise_port), + sock=ssdp_socket, + ) + + +class UPNPResponderProtocol: """Handle responding to UPNP/SSDP discovery requests.""" - _interrupted = False - - def __init__( - self, - host_ip_addr, - listen_port, - upnp_bind_multicast, - advertise_ip, - advertise_port, - ): + def __init__(self, loop, ssdp_socket, advertise_ip, advertise_port): """Initialize the class.""" - threading.Thread.__init__(self) - - self.host_ip_addr = host_ip_addr - self.listen_port = listen_port - self.upnp_bind_multicast = upnp_bind_multicast + self.transport = None + self._loop = loop + self._sock = ssdp_socket self.advertise_ip = advertise_ip self.advertise_port = advertise_port - self._ssdp_socket = None - - def run(self): - """Run the server.""" - # Listen for UDP port 1900 packets sent to SSDP multicast address - self._ssdp_socket = ssdp_socket = socket.socket( - socket.AF_INET, socket.SOCK_DGRAM + self._upnp_root_response = self._prepare_response( + "upnp:rootdevice", f"uuid:{HUE_UUID}::upnp:rootdevice" ) - ssdp_socket.setblocking(False) - - # Required for receiving multicast - ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - ssdp_socket.setsockopt( - socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.host_ip_addr) - ) - - ssdp_socket.setsockopt( - socket.SOL_IP, - socket.IP_ADD_MEMBERSHIP, - socket.inet_aton("239.255.255.250") + socket.inet_aton(self.host_ip_addr), - ) - - if self.upnp_bind_multicast: - ssdp_socket.bind(("", 1900)) - else: - ssdp_socket.bind((self.host_ip_addr, 1900)) - - while True: - if self._interrupted: - return - - try: - read, _, _ = select.select([ssdp_socket], [], [ssdp_socket], 2) - - if ssdp_socket in read: - data, addr = ssdp_socket.recvfrom(1024) - else: - # most likely the timeout, so check for interrupt - continue - except OSError as ex: - if self._interrupted: - return - - _LOGGER.error( - "UPNP Responder socket exception occurred: %s", ex.__str__ - ) - # without the following continue, a second exception occurs - # because the data object has not been initialized - continue - - if "M-SEARCH" in data.decode("utf-8", errors="ignore"): - _LOGGER.debug("UPNP Responder M-SEARCH method received: %s", data) - # SSDP M-SEARCH method received, respond to it with our info - response = self._handle_request(data) - - resp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - resp_socket.sendto(response, addr) - _LOGGER.debug("UPNP Responder responding with: %s", response) - resp_socket.close() - - def stop(self): - """Stop the server.""" - # Request for server - self._interrupted = True - if self._ssdp_socket: - clean_socket_close(self._ssdp_socket) - self.join() - - def _handle_request(self, data): - if "upnp:rootdevice" in data.decode("utf-8", errors="ignore"): - return self._prepare_response( - "upnp:rootdevice", f"uuid:{HUE_UUID}::upnp:rootdevice" - ) - - return self._prepare_response( + self._upnp_device_response = self._prepare_response( "urn:schemas-upnp-org:device:basic:1", f"uuid:{HUE_UUID}" ) + def connection_made(self, transport): + """Set the transport.""" + self.transport = transport + + def connection_lost(self, exc): + """Handle connection lost.""" + + def datagram_received(self, data, addr): + """Respond to msearch packets.""" + decoded_data = data.decode("utf-8", errors="ignore") + + if "M-SEARCH" not in decoded_data: + return + + _LOGGER.debug("UPNP Responder M-SEARCH method received: %s", data) + # SSDP M-SEARCH method received, respond to it with our info + response = self._handle_request(decoded_data) + _LOGGER.debug("UPNP Responder responding with: %s", response) + self.transport.sendto(response, addr) + + def error_received(self, exc): # pylint: disable=no-self-use + """Log UPNP errors.""" + _LOGGER.error("UPNP Error received: %s", exc) + + def close(self): + """Stop the server.""" + _LOGGER.info("UPNP responder shutting down") + if self.transport: + self.transport.close() + self._loop.remove_writer(self._sock.fileno()) + self._loop.remove_reader(self._sock.fileno()) + self._sock.close() + + def _handle_request(self, decoded_data): + if "upnp:rootdevice" in decoded_data: + return self._upnp_root_response + + return self._upnp_device_response + def _prepare_response(self, search_target, unique_service_name): # Note that the double newline at the end of # this string is required per the SSDP spec @@ -167,10 +158,3 @@ USN: {unique_service_name} """ return response.replace("\n", "\r\n").encode("utf-8") - - -def clean_socket_close(sock): - """Close a socket connection and logs its closure.""" - _LOGGER.info("UPNP responder shutting down") - - sock.close() diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index a23296308f6..09791f9d242 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -96,7 +96,7 @@ def hass_hue(loop, hass): ) ) - with patch("homeassistant.components.emulated_hue.UPNPResponderThread"): + with patch("homeassistant.components.emulated_hue.create_upnp_datagram_endpoint"): loop.run_until_complete( setup.async_setup_component( hass, diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index a6040e8db68..8a5556b1222 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -8,9 +8,9 @@ import requests from homeassistant import const, setup from homeassistant.components import emulated_hue +from homeassistant.components.emulated_hue import upnp from homeassistant.const import HTTP_OK -from tests.async_mock import patch from tests.common import get_test_home_assistant, get_test_instance_port HTTP_SERVER_PORT = get_test_instance_port() @@ -20,6 +20,18 @@ BRIDGE_URL_BASE = f"http://127.0.0.1:{BRIDGE_SERVER_PORT}" + "{}" JSON_HEADERS = {CONTENT_TYPE: const.CONTENT_TYPE_JSON} +class MockTransport: + """Mock asyncio transport.""" + + def __init__(self): + """Create a place to store the sends.""" + self.sends = [] + + def sendto(self, response, addr): + """Mock sendto.""" + self.sends.append((response, addr)) + + class TestEmulatedHue(unittest.TestCase): """Test the emulated Hue component.""" @@ -30,16 +42,11 @@ class TestEmulatedHue(unittest.TestCase): """Set up the class.""" cls.hass = hass = get_test_home_assistant() - with patch("homeassistant.components.emulated_hue.UPNPResponderThread"): - setup.setup_component( - hass, - emulated_hue.DOMAIN, - { - emulated_hue.DOMAIN: { - emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT - } - }, - ) + setup.setup_component( + hass, + emulated_hue.DOMAIN, + {emulated_hue.DOMAIN: {emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT}}, + ) cls.hass.start() @@ -50,23 +57,24 @@ class TestEmulatedHue(unittest.TestCase): def test_upnp_discovery_basic(self): """Tests the UPnP basic discovery response.""" - with patch("threading.Thread.__init__"): - upnp_responder_thread = emulated_hue.UPNPResponderThread( - "0.0.0.0", 80, True, "192.0.2.42", 8080 - ) + upnp_responder_protocol = upnp.UPNPResponderProtocol( + None, None, "192.0.2.42", 8080 + ) + mock_transport = MockTransport() + upnp_responder_protocol.transport = mock_transport - """Original request emitted by the Hue Bridge v1 app.""" - request = """M-SEARCH * HTTP/1.1 + """Original request emitted by the Hue Bridge v1 app.""" + request = """M-SEARCH * HTTP/1.1 HOST:239.255.255.250:1900 ST:ssdp:all Man:"ssdp:discover" MX:3 """ - encoded_request = request.replace("\n", "\r\n").encode("utf-8") + encoded_request = request.replace("\n", "\r\n").encode("utf-8") - response = upnp_responder_thread._handle_request(encoded_request) - expected_response = """HTTP/1.1 200 OK + upnp_responder_protocol.datagram_received(encoded_request, 1234) + expected_response = """HTTP/1.1 200 OK CACHE-CONTROL: max-age=60 EXT: LOCATION: http://192.0.2.42:8080/description.xml @@ -76,27 +84,30 @@ ST: urn:schemas-upnp-org:device:basic:1 USN: uuid:2f402f80-da50-11e1-9b23-001788255acc """ - assert expected_response.replace("\n", "\r\n").encode("utf-8") == response + expected_send = expected_response.replace("\n", "\r\n").encode("utf-8") + + assert mock_transport.sends == [(expected_send, 1234)] def test_upnp_discovery_rootdevice(self): """Tests the UPnP rootdevice discovery response.""" - with patch("threading.Thread.__init__"): - upnp_responder_thread = emulated_hue.UPNPResponderThread( - "0.0.0.0", 80, True, "192.0.2.42", 8080 - ) + upnp_responder_protocol = upnp.UPNPResponderProtocol( + None, None, "192.0.2.42", 8080 + ) + mock_transport = MockTransport() + upnp_responder_protocol.transport = mock_transport - """Original request emitted by Busch-Jaeger free@home SysAP.""" - request = """M-SEARCH * HTTP/1.1 + """Original request emitted by Busch-Jaeger free@home SysAP.""" + request = """M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" MX: 40 ST: upnp:rootdevice """ - encoded_request = request.replace("\n", "\r\n").encode("utf-8") + encoded_request = request.replace("\n", "\r\n").encode("utf-8") - response = upnp_responder_thread._handle_request(encoded_request) - expected_response = """HTTP/1.1 200 OK + upnp_responder_protocol.datagram_received(encoded_request, 1234) + expected_response = """HTTP/1.1 200 OK CACHE-CONTROL: max-age=60 EXT: LOCATION: http://192.0.2.42:8080/description.xml @@ -106,7 +117,31 @@ ST: upnp:rootdevice USN: uuid:2f402f80-da50-11e1-9b23-001788255acc::upnp:rootdevice """ - assert expected_response.replace("\n", "\r\n").encode("utf-8") == response + expected_send = expected_response.replace("\n", "\r\n").encode("utf-8") + + assert mock_transport.sends == [(expected_send, 1234)] + + def test_upnp_no_response(self): + """Tests the UPnP does not response on an invalid request.""" + upnp_responder_protocol = upnp.UPNPResponderProtocol( + None, None, "192.0.2.42", 8080 + ) + mock_transport = MockTransport() + upnp_responder_protocol.transport = mock_transport + + """Original request emitted by the Hue Bridge v1 app.""" + request = """INVALID * HTTP/1.1 +HOST:239.255.255.250:1900 +ST:ssdp:all +Man:"ssdp:discover" +MX:3 + +""" + encoded_request = request.replace("\n", "\r\n").encode("utf-8") + + upnp_responder_protocol.datagram_received(encoded_request, 1234) + + assert mock_transport.sends == [] def test_description_xml(self): """Test the description.""" From 42227c1c53c01444291e8c43f7a084576660ba0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2020 02:57:58 -0500 Subject: [PATCH 294/862] Improve the performance of dt_util.utcnow() (#39145) --- homeassistant/util/dt.py | 2 +- tests/components/metoffice/__init__.py | 11 +++++++++++ tests/components/metoffice/test_sensor.py | 10 ++++------ tests/components/metoffice/test_weather.py | 17 +++++++---------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 413c8d4f920..d16e02913b5 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -52,7 +52,7 @@ def get_time_zone(time_zone_str: str) -> Optional[dt.tzinfo]: def utcnow() -> dt.datetime: """Get now in UTC time.""" - return dt.datetime.now(UTC) + return dt.datetime.utcnow().replace(tzinfo=UTC) def now(time_zone: Optional[dt.tzinfo] = None) -> dt.datetime: diff --git a/tests/components/metoffice/__init__.py b/tests/components/metoffice/__init__.py index fdefc3d4786..e3611eb7973 100644 --- a/tests/components/metoffice/__init__.py +++ b/tests/components/metoffice/__init__.py @@ -1 +1,12 @@ """Tests for the metoffice component.""" + +import datetime + + +class NewDateTime(datetime.datetime): + """Patch time to a specific point.""" + + @classmethod + def now(cls, *args, **kwargs): # pylint: disable=signature-differs + """Overload datetime.datetime.now.""" + return cls(2020, 4, 25, 12, tzinfo=datetime.timezone.utc) diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 5d6f2787861..b04dc6b2422 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -1,9 +1,9 @@ """The tests for the Met Office sensor component.""" -from datetime import datetime, timezone import json from homeassistant.components.metoffice.const import ATTRIBUTION, DOMAIN +from . import NewDateTime from .const import ( DATETIME_FORMAT, KINGSLYNN_SENSOR_RESULTS, @@ -15,13 +15,12 @@ from .const import ( WAVERTREE_SENSOR_RESULTS, ) -from tests.async_mock import Mock, patch +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture @patch( - "datapoint.Forecast.datetime.datetime", - Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), + "datapoint.Forecast.datetime.datetime", NewDateTime, ) async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_time): """Test the Met Office sensor platform.""" @@ -58,8 +57,7 @@ async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_tim @patch( - "datapoint.Forecast.datetime.datetime", - Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), + "datapoint.Forecast.datetime.datetime", NewDateTime, ) 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.""" diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 05cec7ef46e..673dec7d5a6 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -1,24 +1,24 @@ """The tests for the Met Office sensor component.""" -from datetime import datetime, timedelta, timezone +from datetime import timedelta import json from homeassistant.components.metoffice.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.util import utcnow +from . import NewDateTime from .const import ( METOFFICE_CONFIG_KINGSLYNN, METOFFICE_CONFIG_WAVERTREE, WAVERTREE_SENSOR_RESULTS, ) -from tests.async_mock import Mock, patch +from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture @patch( - "datapoint.Forecast.datetime.datetime", - Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), + "datapoint.Forecast.datetime.datetime", NewDateTime, ) async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time): """Test we handle cannot connect error.""" @@ -39,8 +39,7 @@ async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time): @patch( - "datapoint.Forecast.datetime.datetime", - Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), + "datapoint.Forecast.datetime.datetime", NewDateTime, ) async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): """Test we handle cannot connect error.""" @@ -74,8 +73,7 @@ async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): @patch( - "datapoint.Forecast.datetime.datetime", - Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), + "datapoint.Forecast.datetime.datetime", NewDateTime, ) async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_time): """Test the Met Office weather platform.""" @@ -108,8 +106,7 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti @patch( - "datapoint.Forecast.datetime.datetime", - Mock(now=Mock(return_value=datetime(2020, 4, 25, 12, tzinfo=timezone.utc))), + "datapoint.Forecast.datetime.datetime", NewDateTime, ) async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_time): """Test we handle two different weather sites both running.""" From b68c5cec94a0ced6facdedcc7f6b3491c16f40a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2020 02:59:26 -0500 Subject: [PATCH 295/862] Convert bayesian binary_sensor to use async_track_template_result (#39174) Add coverage to reach 100% line coverage --- .../components/bayesian/binary_sensor.py | 133 ++++++++++++---- .../components/bayesian/test_binary_sensor.py | 147 +++++++++++++++++- 2 files changed, 250 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 1107a682039..86f11cda7e1 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -1,5 +1,6 @@ """Use Bayesian Inference to trigger a binary sensor.""" from collections import OrderedDict +import logging import voluptuous as vol @@ -16,9 +17,14 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import callback +from homeassistant.exceptions import TemplateError from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.event import ( + async_track_state_change_event, + async_track_template_result, +) +from homeassistant.helpers.template import result_as_boolean ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" @@ -36,6 +42,9 @@ CONF_TO_STATE = "to_state" DEFAULT_NAME = "Bayesian Binary Sensor" DEFAULT_PROBABILITY_THRESHOLD = 0.5 +_LOGGER = logging.getLogger(__name__) + + NUMERIC_STATE_SCHEMA = vol.Schema( { CONF_PLATFORM: "numeric_state", @@ -107,8 +116,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= BayesianBinarySensor( name, prior, observations, probability_threshold, device_class ) - ], - True, + ] ) @@ -122,17 +130,19 @@ class BayesianBinarySensor(BinarySensorEntity): self._probability_threshold = probability_threshold self._device_class = device_class self._deviation = False + self._callbacks = [] + self.prior = prior self.probability = prior self.current_observations = OrderedDict({}) self.observations_by_entity = self._build_observations_by_entity() + self.observations_by_template = self._build_observations_by_template() self.observation_handlers = { "numeric_state": self._process_numeric_state, "state": self._process_state, - "template": self._process_template, } async def async_added_to_hass(self): @@ -166,17 +176,61 @@ class BayesianBinarySensor(BinarySensorEntity): entity = event.data.get("entity_id") self.current_observations.update(self._record_entity_observations(entity)) - self.probability = self._calculate_new_probability() + self._recalculate_and_write_state() - self.hass.async_add_job(self.async_update_ha_state, True) + self.async_on_remove( + async_track_state_change_event( + self.hass, + list(self.observations_by_entity), + async_threshold_sensor_state_listener, + ) + ) + + @callback + def _async_template_result_changed(event, template, last_result, result): + entity = event and event.data.get("entity_id") + + if isinstance(result, TemplateError): + _LOGGER.error( + "TemplateError('%s') " + "while processing template '%s' " + "in entity '%s'", + result, + template, + self.entity_id, + ) + + should_trigger = False + else: + should_trigger = result_as_boolean(result) + + for obs in self.observations_by_template[template]: + if should_trigger: + obs_entry = {"entity_id": entity, **obs} + else: + obs_entry = None + self.current_observations[obs["id"]] = obs_entry + + self._recalculate_and_write_state() + + for template in self.observations_by_template: + info = async_track_template_result( + self.hass, template, _async_template_result_changed + ) + + self._callbacks.append(info) + self.async_on_remove(info.async_remove) + info.async_refresh() self.current_observations.update(self._initialize_current_observations()) self.probability = self._calculate_new_probability() - async_track_state_change_event( - self.hass, - list(self.observations_by_entity), - async_threshold_sensor_state_listener, - ) + self._deviation = bool(self.probability >= self._probability_threshold) + + @callback + def _recalculate_and_write_state(self): + self.probability = self._calculate_new_probability() + self._deviation = bool(self.probability >= self._probability_threshold) + self.async_write_ha_state() def _initialize_current_observations(self): local_observations = OrderedDict({}) @@ -186,9 +240,8 @@ class BayesianBinarySensor(BinarySensorEntity): def _record_entity_observations(self, entity): local_observations = OrderedDict({}) - entity_obs_list = self.observations_by_entity[entity] - for entity_obs in entity_obs_list: + for entity_obs in self.observations_by_entity[entity]: platform = entity_obs["platform"] should_trigger = self.observation_handlers[platform](entity_obs) @@ -233,18 +286,42 @@ class BayesianBinarySensor(BinarySensorEntity): for ind, obs in enumerate(self._observations): obs["id"] = ind - if "entity_id" in obs: - entity_ids = [obs["entity_id"]] - elif "value_template" in obs: - entity_ids = obs.get(CONF_VALUE_TEMPLATE).extract_entities() + if "entity_id" not in obs: + continue + + entity_ids = [obs["entity_id"]] for e_id in entity_ids: - obs_list = observations_by_entity.get(e_id, []) - obs_list.append(obs) - observations_by_entity[e_id] = obs_list + observations_by_entity.setdefault(e_id, []).append(obs) return observations_by_entity + def _build_observations_by_template(self): + """ + Build and return data structure of the form below. + + { + "template": [{"id": 0, ...}, {"id": 1, ...}], + "template2": [{"id": 2, ...}], + ... + } + + Each "observation" must be recognized uniquely, and it should be possible + for all relevant observations to be looked up via their `template`. + """ + + observations_by_template = {} + for ind, obs in enumerate(self._observations): + obs["id"] = ind + + if "value_template" not in obs: + continue + + template = obs.get(CONF_VALUE_TEMPLATE) + observations_by_template.setdefault(template, []).append(obs) + + return observations_by_template + def _process_numeric_state(self, entity_observation): """Return True if numeric condition is met.""" entity = entity_observation["entity_id"] @@ -264,12 +341,6 @@ class BayesianBinarySensor(BinarySensorEntity): return condition.state(self.hass, entity, entity_observation.get("to_state")) - def _process_template(self, entity_observation): - """Return True if template condition is True.""" - template = entity_observation.get(CONF_VALUE_TEMPLATE) - template.hass = self.hass - return condition.async_template(self.hass, template, entity_observation) - @property def name(self): """Return the name of the sensor.""" @@ -307,7 +378,7 @@ class BayesianBinarySensor(BinarySensorEntity): { obs.get("entity_id") for obs in self.current_observations.values() - if obs is not None + if obs is not None and obs.get("entity_id") is not None } ), ATTR_PROBABILITY: round(self.probability, 2), @@ -316,4 +387,10 @@ class BayesianBinarySensor(BinarySensorEntity): async def async_update(self): """Get the latest data and update the states.""" - self._deviation = bool(self.probability >= self._probability_threshold) + if not self._callbacks: + self._recalculate_and_write_state() + return + # Force recalc of the templates. The states will + # update automatically. + for call in self._callbacks: + call.async_refresh() diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 7bbb9eeda27..9e4983ab4d5 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -3,8 +3,12 @@ import json import unittest from homeassistant.components.bayesian import binary_sensor as bayesian -from homeassistant.const import STATE_UNKNOWN -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, STATE_UNKNOWN +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant @@ -488,3 +492,142 @@ class TestBayesianBinarySensor(unittest.TestCase): for key, attrs in state.attributes.items(): json.dumps(attrs) + + +async def test_template_error(hass, caplog): + """Test sensor with template error.""" + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "template", + "value_template": "{{ xyz + 1 }}", + "prob_given_true": 0.9, + }, + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_binary").state == "off" + + assert "TemplateError" in caplog.text + assert "xyz" in caplog.text + + +async def test_update_request_with_template(hass): + """Test sensor on template platform observations that gets an update request.""" + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored') == 'off'}}", + "prob_given_true": 0.8, + "prob_given_false": 0.4, + } + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + await async_setup_component(hass, "binary_sensor", config) + await async_setup_component(hass, HA_DOMAIN, {}) + + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_binary").state == "off" + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "binary_sensor.test_binary"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.test_binary").state == "off" + + +async def test_update_request_without_template(hass): + """Test sensor on template platform observations that gets an update request.""" + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "state", + "entity_id": "sensor.test_monitored", + "to_state": "off", + "prob_given_true": 0.9, + "prob_given_false": 0.4, + }, + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + await async_setup_component(hass, "binary_sensor", config) + await async_setup_component(hass, HA_DOMAIN, {}) + + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_monitored", "on") + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_binary").state == "off" + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "binary_sensor.test_binary"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.test_binary").state == "off" + + +async def test_monitored_sensor_goes_away(hass): + """Test sensor on template platform observations that goes away.""" + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "state", + "entity_id": "sensor.test_monitored", + "to_state": "on", + "prob_given_true": 0.9, + "prob_given_false": 0.4, + }, + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + await async_setup_component(hass, "binary_sensor", config) + await async_setup_component(hass, HA_DOMAIN, {}) + + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_monitored", "on") + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_binary").state == "on" + + hass.states.async_remove("sensor.test_monitored") + + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.test_binary").state == "on" From 2bf31dc5b3310d39005aa7f11bc0727012b423df Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Aug 2020 12:08:52 +0200 Subject: [PATCH 296/862] Upgrade mutagen to 1.45.1 (#39166) --- homeassistant/components/tts/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index a53357b1a2b..3db130d01bc 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -2,7 +2,7 @@ "domain": "tts", "name": "Text-to-Speech (TTS)", "documentation": "https://www.home-assistant.io/integrations/tts", - "requirements": ["mutagen==1.44.0"], + "requirements": ["mutagen==1.45.1"], "dependencies": ["http"], "after_dependencies": ["media_player"], "codeowners": ["@pvizeli"] diff --git a/requirements_all.txt b/requirements_all.txt index f9bdd548fdb..697b228bf5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -923,7 +923,7 @@ minio==4.0.9 mitemp_bt==0.0.3 # homeassistant.components.tts -mutagen==1.44.0 +mutagen==1.45.1 # homeassistant.components.mychevy mychevy==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6810faae9e0..aaa45891f18 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,7 +442,7 @@ millheater==0.3.4 minio==4.0.9 # homeassistant.components.tts -mutagen==1.44.0 +mutagen==1.45.1 # homeassistant.components.ness_alarm nessclient==0.9.15 From d31dea50bb13138b0ab05186546280500657652d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Aug 2020 12:11:59 +0200 Subject: [PATCH 297/862] Upgrade discord.py to 1.4.1 (#39150) --- homeassistant/components/discord/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 1f4ccbdf5f5..ccaa595f126 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,6 +2,6 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": ["discord.py==1.3.4"], + "requirements": ["discord.py==1.4.1"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 697b228bf5c..73231341f31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -488,7 +488,7 @@ directv==0.3.0 discogs_client==2.2.2 # homeassistant.components.discord -discord.py==1.3.4 +discord.py==1.4.1 # homeassistant.components.updater distro==1.5.0 From 1126c750e121be6903ffbc5971e93a63041b22bd Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Aug 2020 12:25:54 +0200 Subject: [PATCH 298/862] Upgrade colorlog to 4.2.1 (#39159) --- homeassistant/scripts/check_config.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index e96bc57624f..487d97ffdd8 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -17,7 +17,7 @@ import homeassistant.util.yaml.loader as yaml_loader # mypy: allow-untyped-calls, allow-untyped-defs -REQUIREMENTS = ("colorlog==4.1.0",) +REQUIREMENTS = ("colorlog==4.2.1",) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access diff --git a/requirements_all.txt b/requirements_all.txt index 73231341f31..9968ed8db92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -429,7 +429,7 @@ coinbase==2.1.0 coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.1.0 +colorlog==4.2.1 # homeassistant.components.concord232 concord232==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aaa45891f18..a1e9c2c64bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -217,7 +217,7 @@ caldav==0.6.1 coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.1.0 +colorlog==4.2.1 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From f295684c106e0135b23f742d727473530b156cad Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Aug 2020 12:27:53 +0200 Subject: [PATCH 299/862] Upgrade TwitterAPI to 2.5.13 (#39157) --- homeassistant/components/twitter/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index 81048758889..044151094da 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -2,6 +2,6 @@ "domain": "twitter", "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", - "requirements": ["TwitterAPI==2.5.11"], + "requirements": ["TwitterAPI==2.5.13"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 9968ed8db92..34bac40ade0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -85,7 +85,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.5.11 +TwitterAPI==2.5.13 # homeassistant.components.tof # VL53L1X2==0.1.5 From 62b1f23328dea57c4e67365f5631dbcc6dcbf7ac Mon Sep 17 00:00:00 2001 From: "J.P. Hutchins" <34154542+JPHutchins@users.noreply.github.com> Date: Sun, 23 Aug 2020 04:29:44 -0700 Subject: [PATCH 300/862] Allow multiple config entries per host for transmission (#39127) * Allow multiple integrations per host (check port) #36605 * Add test for allow multiple config entries per host for transmission --- .../components/transmission/config_flow.py | 5 +++- .../transmission/test_config_flow.py | 24 ++++++++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index c457306310d..8b43850623a 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -56,7 +56,10 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == user_input[CONF_HOST]: + if ( + entry.data[CONF_HOST] == user_input[CONF_HOST] + and entry.data[CONF_PORT] == user_input[CONF_PORT] + ): return self.async_abort(reason="already_configured") if entry.data[CONF_NAME] == user_input[CONF_NAME]: errors[CONF_NAME] = "name_exists" diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 6b9917d17f4..0820e71ae3b 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -204,13 +204,31 @@ async def test_host_already_configured(hass, api): options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, context={"source": "user"}, data=MOCK_ENTRY - ) + mock_entry_unique_name = MOCK_ENTRY.copy() + mock_entry_unique_name[CONF_NAME] = "Transmission 1" + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, context={"source": "user"}, data=mock_entry_unique_name + ) assert result["type"] == "abort" assert result["reason"] == "already_configured" + mock_entry_unique_port = MOCK_ENTRY.copy() + mock_entry_unique_port[CONF_PORT] = 9092 + mock_entry_unique_port[CONF_NAME] = "Transmission 2" + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, context={"source": "user"}, data=mock_entry_unique_port + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + mock_entry_unique_host = MOCK_ENTRY.copy() + mock_entry_unique_host[CONF_HOST] = "192.168.1.101" + mock_entry_unique_host[CONF_NAME] = "Transmission 3" + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, context={"source": "user"}, data=mock_entry_unique_host + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + async def test_name_already_configured(hass, api): """Test name is already configured.""" From 961f36c679f4bd70ba4a9ce2e211c09713ce2ee0 Mon Sep 17 00:00:00 2001 From: Fritiof Hedman <989352+zyberzero@users.noreply.github.com> Date: Sun, 23 Aug 2020 15:16:26 +0200 Subject: [PATCH 301/862] Add ZwaveStringSensor to OZW integration (#38676) * Add ZwaveStringSensor to OZW integration * Remove unnecessary new line * Set enabled default to false for ZwaveStringSensor * Add missing decorator for property * Add a test for ZwaveStringSensor * Also test state of ZWaveStringSensor * Remove entity type check --- homeassistant/components/ozw/sensor.py | 22 +++++++++++++++++++ tests/components/ozw/conftest.py | 6 +++++ tests/components/ozw/test_sensor.py | 21 ++++++++++++++++++ .../ozw/sensor_string_value_network_dump.csv | 5 +++++ 4 files changed, 54 insertions(+) create mode 100644 tests/fixtures/ozw/sensor_string_value_network_dump.csv diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py index 453015991b7..bbb13352b0b 100644 --- a/homeassistant/components/ozw/sensor.py +++ b/homeassistant/components/ozw/sensor.py @@ -36,6 +36,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif isinstance(value.primary.value, dict): sensor = ZWaveListSensor(value) + elif isinstance(value.primary.value, str): + sensor = ZWaveStringSensor(value) + else: _LOGGER.warning("Sensor not implemented for value %s", value.primary.label) return @@ -93,6 +96,25 @@ class ZwaveSensorBase(ZWaveDeviceEntity): return True +class ZWaveStringSensor(ZwaveSensorBase): + """Representation of a Z-Wave sensor.""" + + @property + def state(self): + """Return state of the sensor.""" + return self.values.primary.value + + @property + def unit_of_measurement(self): + """Return unit of measurement the value is expressed in.""" + return self.values.primary.units + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return False + + class ZWaveNumericSensor(ZwaveSensorBase): """Representation of a Z-Wave sensor.""" diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index 8ad19aca46c..3e30b60129b 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -87,6 +87,12 @@ def lock_data_fixture(): return load_fixture("ozw/lock_network_dump.csv") +@pytest.fixture(name="string_sensor_data", scope="session") +def string_sensor_fixture(): + """Load string sensor MQTT data and return it.""" + return load_fixture("ozw/sensor_string_value_network_dump.csv") + + @pytest.fixture(name="sent_messages") def sent_messages_fixture(): """Fixture to capture sent messages.""" diff --git a/tests/components/ozw/test_sensor.py b/tests/components/ozw/test_sensor.py index 4cc0077cdea..91de895648e 100644 --- a/tests/components/ozw/test_sensor.py +++ b/tests/components/ozw/test_sensor.py @@ -74,3 +74,24 @@ async def test_sensor_enabled(hass, generic_data, sensor_msg): assert state is not None assert state.state == "0" assert state.attributes["label"] == "Clear" + + +async def test_string_sensor(hass, string_sensor_data): + """Test so the returned type is a string sensor.""" + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry = registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "1-49-73464969749610519", + suggested_object_id="id_150_z_wave_module_user_code", + disabled_by=None, + ) + + await setup_ozw(hass, fixture=string_sensor_data) + await hass.async_block_till_done() + + state = hass.states.get(entry.entity_id) + assert state is not None + assert state.state == "asdfgh" diff --git a/tests/fixtures/ozw/sensor_string_value_network_dump.csv b/tests/fixtures/ozw/sensor_string_value_network_dump.csv new file mode 100644 index 00000000000..071d92da0d0 --- /dev/null +++ b/tests/fixtures/ozw/sensor_string_value_network_dump.csv @@ -0,0 +1,5 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1240", "OZWDaemon_Version": "0.1.170", "QTOpenZWave_Version": "1.2.0", "QT_Version": "5.12.9", "Status": "driverAllNodesQueried", "TimeStamp": 1598022319, "ManufacturerSpecificDBReady": true, "homeID": 3389163831, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} +OpenZWave/1/node/49/,{ "NodeID": 49, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0373:0001:0003", "ZWAProductURL": "https://products.z-wavealliance.org/products/2780/", "ProductPic": "images/idlock/idlock150.png", "Description": "A module enabling your ID Lock digital door lock to a Z-Wave Plus enabled digital door Lock. The module is compatible with ID Lock 101 and ID Lock 150. It enables your ID Lock to operate in a Z-Wave network with numerous access control funtions and notifications.", "ProductManualURL": "https://idlock.no/wp-content/uploads/2019/08/IDLock150_ZWave_UserManual_v3.02.pdf", "ProductPageURL": "https://idlock.no/z-wave/", "InclusionHelp": "Inclusion – (Puts your device in inclusion mode) • Push and hold key button until all LEDs on touch panel activates (with ID Lock in an unlocked state). • Release button. • Enter Master PIN followed by * on touch panel. • Press digit \"2\" for settings followed by * on touch panel. • Press digit “5” on touch panel. Inclusion mode starts immediately. LED indicator below logo signals this by flashing blue.", "ExclusionHelp": "Exclusion – (Puts your device in exclusion mode) • Push and hold key button until all LEDs on touch panel activates (with ID Lock in an unlocked state). • Release button. • Enter Master PIN followed by * on touch panel. • Press digit \"2\" for settings followed by * on touch panel. • Press digit “5” on touch panel. Exclusion mode starts immediately. LED indicator below logo signals this by flashing blue.", "ResetHelp": "Device reset – (This will reset RF interface module to factory default settings) Warning: Please do only proceed with the following reset procedure, if primary network controller is missing or otherwise inoperable. RESET Z-WAVE MODULE: • Push and hold key button until all LEDs on touch panel activates (with ID Lock in an unlocked state). • Release button. • Enter Master PIN followed by * on keypad. • Press digit \"2\" for settings followed by * on keypad. • Press digit “0” on keypad. If the Z-wave module is not included in a Z-wave network the door lock will also return to factory settings when following the above procedure. FACTORY RESET DOOR LOCK FIRMWARE: • Push and hold inside lock/unlock button while inserting the fourth battery. • Receive reset sound. • Release button. • Receive confirmation sound.", "WakeupHelp": "Activate by touching the touch panel with finger(s), the palm of the hand on the outside unit or by pushing the key button on the inside unit.", "ProductSupportURL": "https://idlock.no/kundesenter/", "Frequency": "CEPT (Europe)", "Name": "ID Lock 150 Z-Wave module", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACaCAIAAABjfJA1AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nOy8Wa9lR3YmtlYMezrzcMe8U85MMjmzWMUqUlWqQaUqSQV1q9GS5Uaj3XADNgw/+MkwDFt68pP/gNsG/OiG293VakHqdqnmSUUWyeSUyUzmdOfp3DPueUfEWn44ySyKUhs2bMMwrLjAAfa95+yIG/tb3/rWEAeZGf6vDWZGxH/X5f+Nb/4/tYy/nff/lXkfDyQiAEDE+Qcev/4fnOZvx/+fx/8OyP5mDBHRaDSy1v4/vLC/Hf/fHs1mMwzDObbmQHqMM/XJ9zEzM+/vH/x3//SfPnzwEMgRESJadEop4YhRAqJzDgCBQQjBzPObMeCc6vjju5NzwAwAQkgUKIVktohAjqx1iMAAAqVzpJR6vCAGkgIB2DkGEADAwICMACgEMAIDIAgUACAEOjLzNSihiIjIoWBmFkKSk8wshJjfGREdGCYSQgiQQiAzExMzC4HIQkqJiM4Z5xwzC6kQ8fGuIQpmojmjEyGiEIIZiBwTCykAEef/OuLjeZkZQTrnhBDMVkiUUiJKKaUxhpmttUIIrRWRIyJgkEIQEQPCxw4EEZRS5BgBAIGJEZGI5rvLQI92H5GIUEprDCIKwUppRCRyzrn5DjyaQsn5TTzPA4D5Xx8NgVJKay0wO+tQCCEUorDWABIACCGklPOPdDqdb33rW6+++gUh8FOO7q+4QgCYzWb/1X/5Xx8fHzOzEEDzxyA1Azhn0bGQkpwjBkRUUpEgmMMHkByLOYjmCCNyziEiAwMKRBCIwMDMxCylAABGJAYpBBMrKYUQDPyrXXDEDEIAijnugemRAQghHmORiKUUSgtnLTMz48dIYuccCjGHgtKaUTwyLHYACMDuY1IW7FAIrTUjVVXFzIjy0ZuFmO8S4MdTP7KmR6blnJOI8PEv53ZimUDMHz/D/AeYmYQQUiqAX3kQIhIC50B31kkhAMARoxBzdFprlVJSCGcdMyul5suYPx1mdo6MsYiolEIhichZy2znu4SPuACUUtZaIpJSaq0RUUr5GFjzuVCKR0vCx0NrrauqMqYgJqWUlHLOOHPr+sM//MMvfOGVTxohAPyKKuZoeO+9904OjwSiQGHZzd/kr0XoK8fESMSEzkkQTIRCSBJzC0OQzMDMnpQCBTkHDGKOV2Kczzc3Q4EKBDgiIkAUUllrmR6ZnXUkWAkxB+KjTZdSMLGx1to5iQoiImAERBBaKiUkMLBABhZijgwQQqAS84lRIUhgZ6WUzEyEc3p0wjKxEIIBiRywUIhKKeecEHMCc0JJLT1rzCd2E+e4JGYE0J73iBeZiT8GPwIwSymtNQyECACotWeMcc5JqeYPeM4izs1ZkueomhsYAzw2e2stIc53mEwlUDgiYDLGSSURBQAQs3VOMAshtKetZXJuznlCCEC01qJAJdUclI+YCYCIGMCT8rHjmu8AAyAAEllrEdEP/Pl65qh4xBrM3/72t9vt5vXr1x8D6dOuEACGw6EFQkYEJClZidWXz6dLJneVEwAERKSFUEKWFRsmAvaFRsGEQpOQnveIDZ3TUiMDEjFYED4wSmAFIvA8jdJZa61lENrzEREdS5QIgIBKKSWVdZaApRQSpYRHvlaitNYCIjBIpZy1zISIKNAYEygPWQDynHKcc845JZW1pLVm5kZUR0SBoizLsiwQhed7SZoKRHYYhkGe50islRZSlFVJREpJwWyMAUAByvf92XSKkhHRkSuMJVtJXzpL1laATCScI8cExAjITAAoBCqlXGWZaP5ItNZERI4QwBEh4By1jmmuJRQTzsmbEVkAAjmaW8Vjj0OOwDmHIISQJBw5IkZmCw4BEKVCxcCVMQ5YCvSUR+SIWAI6IkJnGYUQUmjnnBGEAEI45wi1ZiUAGBAcWCYLjpCFj6q01ZxLrbVKKnIuS9Mf/vCHTz311CeFvHqsuR4vV4pH9MjIS9eXsqWc2K2FvY3lC0fHh/UoSk05Gg+6YRgXU3CFYN0Nl7RSk8m4G3WG4xNHjhEkEAOURaG07jS6w+GZA8dKUZGDIyGEI2LG3OVKqjmZzfVKFIYud9YarRQAWCJGQMTA8yWjsdbzPN8PnM2JnJColAIA3w+V9lcWl52p5haZ57nWOs8LYG42m1p79SACBET0tWednU1ni50VKaVA4YwLw1AqObdg51zNDwWiMSavyjAIGNjTgXOuKEup0DlnTJVCWZRpTtVHh/en8VhKEKgB0Dk7V35zSUDMAgVIJOY5gJhorvCkks45YJ77NWKeq0CplDVmTgFSSOfcY2UJxOycEAKFqKyZc7MSgoicdQLFozmJ50h1TM6RFKgB5n4TQSDAnG4R0RHNfZ8QgshKIiklsJNCWmsF6Mjz0RDlxLE1hyVaV5YlP+JPFkIOh8NP+sFfMdbj6/maUCAgaKDrT5zfqfafaqxwQjiMuVTLOlJRdyehzeW12eR4lswKpS4vXkgmo6QRbS1dyKPeYHyU2rLf7iqpBqOhQH1x+fJA1EbTs1azGflBnKUlOwBwhFJ58EjvkhDIxEAkAoFCmEeq85EUAAYAlGGAKBGFFYxC+Ep5vjeLZ5G2Hgt7fOQAfN9HJt85yrKW71vnaDKZFkWp/MAPmo3GaBYXReEHflxZR46ZQy/KEPIsV0J0u11rnZGamH3P86WanJ0YY8Na5Pu+qarMOc/3pcAmYMTexNnZeGipVKEHZJ1z2vOsc8SMCCgBmZWUjgiApEQAJHYokREcl4gECFIAgENgEgRSMluhCJgRUACgemz8zBKddIwCAQXCXJMQIigUmhwbmAc4c7XpSAr0lCTn2JFAAQQENH/ijAwIQDQPcFgQATIJJ0ACAhM4RmErDcSsGDvYqwXe8P5k7rmrqtLaEx+z0ifF+68ipk9qybnCiGo1BBuWriej6eEJOhdIGY+Hxzt3ymIKaX784QPFniKf0uT07oOmbrgsvXfjA6gEO2xHzYc3bnkOW7X6ye0Ps5NjUxVtXb//5k1tmSrrjHXOVmVRVYVzlhmMsY7YAZTWGmtNWbJzPN+R+U4556xFYCBSAHWpmo6zh3sRQGRpdud+RAZdlaZTawquSjZFkcycqcCZACEyaTl4YLOkE/nVyXaN2dfSzUbFyS6ZNAKaHNzzVVWVeXx4d3D/FtMsHh/ceeenXE0cxWcHtwe7d5yJPaiO794sRkdFfJQfPMgPHybTk5Ly0uTELvC8tldrqEgqUMhaCqWEkKwl+RoFskKnJaAAgaClDHwtlYh0WFe+L0iDQZdrSYohlL4nAbjUgpRQhM5ySeQI2LBzVBIUIC1DBVgCV+Aqj0E5KRkVk3RGMQcI2jmsKkEkEZRkBEY0ApwkA7ZSQAggANiRZKPQSjaOnWHLSBIdkFFIKF3lTZee7CxvdLQUnqc9z/84isJPAUn+8R//8SdR9dFHH92+fXuu71bP9RY2GzuDvQe7u6PEjWOzvHL+0qVLYb1bkKh3e7pZO4nPEKOF3mrY7E6yotZodnqdjNkPOoHXee7ZV1A3Ot21CxeeGseZrkXajxZXlk9mYyNFRWyZHotKJiZiay0zELFjskAOmIWwzhFCZa1lYgSCud8kQDy3tlY6lxkT1Rora+ujNJ6UeWVNZYwBMkwOuCyrZrNtDfXaC8zCODTWdrr9cZrHadLtdIuimualY5SeSuJsMpv1+924mO2f7JdlFnhBmhVFWfnSz/PyZHRmTVUUaVLEo9kITOlJvHO6DZoFu1CoTr230DonFI7TIbBDx4HvN/zWcntFOCEIAhW2W50sTfvtrknLdq3lsX9p9XI9iMazIYEFZgGurutPXXi2FXWyWba5srG1dMFVpakyQKvI1rVeqvWxouX2onbBE1tPt4OWTaqr559s19tZOg0i31RlPaoHniZniIyW6vK5Jza7m4JlUcxYkhQSERBZCCMFITqJJJAFMiBIBCVACJIISomAiCUrqaMiSsaZI0JmgQIR6436l770pTmw5kN9KkHKzMYYJRUAPPX0ddaZQb17chpw9w+++HJTF9odNaV4YmVl5uT7N3eee+65fnu5yPOwVau3RafZVmpj8tGdyAsUB7sHQ3JMZZZMsk5nCyV5SpKoxGTsSSy5qsgIgSjVXNMys5RybgTOOQUohEAUjIAMc+kqUACDE8BEJdi7BwcssAQ8imdja0qyhKxRSilBCCJXWcME0ySN/MagYqkbpS2KIosazUyJqizsbObXm/kkWW53Kxvu7O0oLU6tmaKcVSxrQauzeHR4HAbepUtPTCeTg7sfZGm6sbp2cHJQlE51m6UrTcWqIBlJcnA2GljiJJ06rpwjwSBz4esFXzQlmrJMlhcWmVk6AUZI8LQIZ/lZXEyPhrslpXODKa1TGOwfPNQq1MrPs+xscGeaDxiNBElMitRya9MVhyu982M5un//wyis1Wr1o6NDAlELmiQNkFDog2WFzlCJYLNsdjw7iO3MQYVOGKrmSUhABwyAaIWYB7caaI4IAodKkakMI0qJ0gJ41rm5mTNZ7XmfxM9f0Vj4CTfJAMZZIrrx1rtPvLpRFgUb/Po3vnouKnpi/P3v/vj8+vpnvvDFuwNaX7sMTv305z8zVekrj4Cfvf50p9navXO7FoTf/MY333rrzbfefIuZvvnbv+X73u7u9mB8duHald0Hx+2FVq0ZIIMELaQEIALjrGNGYCWEEEqTsww8T4/NLQMRyTlAVAigpZSq54fC2pmnqixrOwianYPJmQMnhGT4OJeIQOwqU7RDcFkS1EK2gPmkrlTMAssCbREGcnv7Tgi2Fei4NOVkFCl0gbTZbApWapeb2a3777M1QklmMzjbJ3KooUhGVlphCzbMVoISWvdQyKJMUdByb72u67vHO9rT2SzpN7rxcLLZ38yLbHR2OJudLvQ2k3QKXEoGNo4tSBTIMM+GbmxeLHN7dnLUbdYbUWscH7FkRw6RbOWmcbHcWx+OpoXLZ/mkcEWvs2KrNM3SZqOFFiQhuNIRMhMRCSfa9U6gwunBacWVBMVgAZGYEfhRfO0eaTNCSeyYnVRiHkY4lLLigk2WJMQMDFJKkKiUeuzyfpV5/1Qea55XraoKABqtti+9GuvV/kIvgKvnu//Ff/LfHp1kgfeBoej6y69cPb9knXv62sXLm5drgX/r3l2tPSngt37rm0WcUFWhcUUcK6kWWp28qr78xde+8+PvKS2+/rWv/ss//RcbW6tSKyVlVVkhBDuWUjPNoygWAnmeFAVnbK6UAhDACgCUUAKVI8fGtZr+/e2Hut1u16N4MCS2loAFzBE6Zz4UwtqKjZPghgc73c2tld7i7RtvrZ3baPSX8vHZ0d0Ha1eeXFlavPnWTzY2L9UaDWmq/e2H9aUeKjnaPV7Z2CSlcDobTQbtpXPtev3OB+9euHQlFx6PRooqxaK0VFZWCduv13v+ggirg9nO8cmuREnGlUlSGoNBBdq8f/ftNM/8mswmhR94J6exYyJwtaCV29S6nJkFkqP8g913fF1jH0bZZDgagnRATMSAVIlskh85amZ55vvBxsJ5BKyKKkSv3oxKk3pa9mqLQRCNJietsOlpmZtsPBklSYrokSsQieFR2CrEI05x8zQcgGRH7IQAZocoAJiIUUhjyyrHPMuBWWsPEcg5hl/Vc+aZs0+nG+av8ygMWTgWGZHQARgGUS/CpeBchGRGmY1CUZW5c/Jwe3v3g3dbjca0cpeuPlkpvP/gKJ3MyMK9e/cOTo61Um++c8ML/KyMra0Y3GiYfPaFz999eGdptd3udk/PBvV6oypy50hqlELMCxFKaeccokfkP8pdA0ophZhXRYS19tRaXFyIK+csN1fXJnFsCYRQjEpJZGYp0LEDFAzoL/Tanhgl+Ww6ufa5z+wfH89Ot50rg63OneFHfly78tLzpyfD2Ti9sLKy9dTze6dH2qnrr33+YP+wTLOtrY22W7m3d1gQvPq1r+8cHB0dH19dW6+3GrXBYLnf9zyP2JwNj9I8rmxuTSWNCGSk0B8OD2OXHY5JCFSsFAa5Rbbuo7vvOLICZDHKWrKdlFOSSkrJ7ExB4MqYYgdUVQycQ6Uk60ADM1prx7PDsT3S6CnhdXtLzrnB2c7K8gYzDcfHUul+f3kyG+eFWV3orfY2Hjy8A4Y2Vy+ejvZ2TmJEZyTNh5Q4rztZZ+fOwYJgZkIGZGYnpQCwjtCqiFmbec3HkRDCD4LHfvAxttRjSOHHlSkhxDxVsrq6XBa5tWY6SAqQWRX8+//g3/vX3/43rXrrK19+6WCUHB6dNVqLv/zFW69+9sWNjc233785Gk0dy3/2P3+b2R7uHwwHoyRNlBIpm8Wlxc+98jnta0EyLYwOauubl4IwSJNci6DmNQMZzuKJlKi1FkLMq3golFYaURhTKSmRgVkQo5AIzJFf86Rc6S4ejqe2shxJvxZVMc0LWoiAKJWSXFaACBL2T3c1gUFryvLgJMudcWydK3dPBlVVLC2vH0/GDo3UdDo5DbzAKutcdfvBR1VZsqP3t6eCOCvLEMP3HtyM0yTDcnu4jyNV85v9xpLWgaNyVtybJGdElhxgii51v/G13zACXr/z+mG5TyWak+K1555ZX9u6tf/hzdEdIhKxU2yXlxpRc0M1PPRUlbvJ4ejJzaun09ObZ+8lJpZORia4tng1rDeiTvRwtPfR3p0mN873zns17+HZw5NqX4IbHhwsR2tPd68fJUcHRw8rYp+9eOfI6yyFqA4H27tn28Jx1zZWe6sFF/eTnRILYVVfdBeaK4Vzx/H9iksCbFJrobVMSIez/ZQLdCg8aYFqvUarWcRpSsYRkc/wyQrVHEufjgpv3bp148Y7zA4Qzp/fDFre8fhsNJwxy06zu74cfvM3P/viC+dv3Ts6msl/+xc/2VjorPb72q85Uu+8f/vc+talS0+12q2Nja0rV69eeeLqhUsXL12+cvHy5Zde/mxrod/s9QgxTbOdnQdRGBBSWhaM2Gg2syKzZJXSvg58L5yLdHLkeVpIsM6WVYlSKU85dtYaBJDkRBzv3P6w2+16zEd37y00GgaQmQSKOcUxMxMDgJKiyXD6YGd5cUE4N9o77NUbCqGv/Wo42dy4XEeRHRwuNruaOGIcn5x2m3WsTHZ8vNTvlCarWeayCKMgQCgHw167CYbrAFglD053DydHk+R0Gk+MKQmIiHWOZj8e7J0+/+yLT2xe3Vhdv/XRncP7R4cf7l2/fP2Vz7xy4dz5o+OT09PR/Xfu1UTw+7/7exdXz0/TyS/feeOdN99IB+O/+82/8/SF6712/+6De6fbp9lB/LVXv/bqZ14NdPCXN/5yODg7vX346ouvfP21b2wtbR4dHs4G09HO9OLy+d/7+u9dXL8Sx8lgcJbuTdeaq9/6+u9eP3/dOT48PZodjuWEv/LSV37txS9LGW4fHeCQ26b91c98/cvPfTFU4c7hPo/IG8vfeOnrX3vpqzUV7YyPDeUsWAsdFPXZ4YwAtNRaK8/zGs36l770pTmQ5q/yj/7ojz7pCj/88MO33npbKYnAvX7Xq3s7J0ej4XB375RASb+zfTg+GNgUG3/2vZ/t722//JnniyxVnk7yIiny9fW1b//pt5P8tNNr9peWO73e8urKuXNrC4tLXhAQEThmS/3u4uba1tlguL62ubSwEuqo2+x60quHjaXu8mJ7uV3r9JoLveZir7nYbSw0w06n1vdk5PuhkorZOWecrYyxS6vLXhhmDF4QnL90aZjFuTGMOC9izOtZUgilFBMvLa00W92zLMMgePLa9UmSWmuXFlfrnX5WOOn5Vy49kVa2IFpaOtdtdQeTqbNw+dKlOC9mSba4uNjr9ifTqef7l1e3wNNJni+2emurmx/u7JbOaF9nxXShs5CVmTUmzIPTB0en00GtFjR9/1x/+d79j2784sbw8CQpU0emW+/Uo/CNH//s4Z17h8c762vnQvC2Nrd+/uOfv/nzNw+PDpqtqFuLNla37n94/40fv36ws+MFsh5Fy71+Gic/+8FPdx8+SLPp+fWNyAsWWyvf/fPv3Xz/1ng8OL+27oF8cuvq977zndvv3trd2716+XJdhs9efe71n/7luz+7cbZ/sLK+0Gu0Xrr60t5HD9780Zu7d++tba1uLq9fO3d1Mpj95Ds/2rl7P2oGT1156srSFSXVh0d3iMATYa1qxicJA0RBFEWh5/tB6D9ON/zNRWghxLyVoqro3Mp6QmNHFSCULn/jxpul5WuXr6R5fuPGz+7t3CWuIJIypFyUKxcu5EAy0ARJa2FZeNhd6KbTWWUMCyGEkAKuXLg8noyzNNMKyzzrdzvMZKuy3ajbqnKFnSYxkvrp279QLDrtbpLEtahGAGWePvP09Tw15zZWTk5PO7U6tmg6OTubTo9ns3a3LZLcAcTWelFHVFOWGPp6qb84Phs6KktnS2c1ygdnA3AmNZV27sPDPeOMdXR/eIpCpKZwDsvhmWPHtjo+2QdP5FVR5Obmg4dJmhDzIZ56yhcYtupLurmg81RjCl79ZJI8cen6YDqUvkyq5PjklIWjijDzi8x85nOfX1xaNQzG2qVwocG+7PS/+NWvxpNYCQhZiZlrot89339w8mBrcTMdJXZUtWXTW4v244Odw23P93uyHpSe7nVzVQ6GRz6LDocwcaEfqra8cfuN6xvXhfTMWYzGXb72xHQ2pLI0VdePuYrT+tbSjY/elklRlXnLBRAXeqN+d3inv91jwytRLzsZc5PeP75xYX+96y/2vdbscEJNdze/97M3/+K5tef7sqmMX/kFIHoSXVkZV4Jzjnzf9z8ZEj7q+4C/NhCRiBGFJVKeJnZa6JX++n/0j//J73/1xc9f7HzxycX/9D/4/X/4936/riM7SiZHg6c2n9xYuvylV7+y1F8CiRaBpBiejZ+8fG2lvXhxdf1zz7+gEfNsdnKy325FokzcdOCmZ/1QX1haHB0fsCnPTg6TwbEs05YSX33ttQur5/bv3T+/uvo7X/n1tU59sH234audnV2BQjie7ux6WbEShPW8yPcOFrXn0jg/POg4FyL0UDRmOc6KwGKQmUXPDyU6UzSsbVZmOYzagqIiWw50qKBGpp6n/cgLXanyuM3c97z4+FSldHB/f3fnyPM6QjRbzdXx0P385x+Mx4apNo2dkq1uZ8O6YHvn7ORkiIxVYabjWZzE5KxfiWKUALiY89NiPB4NFWAovX6rs7axtnt6GFd5PB0ttFoLzdbK0lJvdfEsH1u23VZ7Y3Vla2396aeerjf741Ea+Y2Nla1IBt1+NxPVWT5bWl7a3Nxc7fTXzi1TXT443RESVrr9cysr57e2VrcuPDwdEMvVhXMXt670F5ZaK0u6Vd8/PV5c6D1x+Wq/v9DbWh8rt3282+u0r1643F/orF7dnNjk5s6tej3YWju31Ol1t/oz7d7bvYWSl5vdrgpNlVubSy0cO+dMWZZJEpuqmpfbPjl+5Qrn17dv337nnXfngeGFyxemZrx9slfmxT/5g390qU3rrfT2mz9QdnxxtVurLZWse72lxmI3NUZodXR8EEbhR3dvNlphs9aO/Kgbhpqo5vtVXg6Hw0arBYyB8AZ7x7PhzORWCi2lOh2NlO/tbT9caTVOdx5aY/Z29j/88MPbH948PDh48OAegm21mpM4zYxpt9u+pwPJWTKL6o0wCibjESpfCBlIWWRps7vYaTbGgzOvXstN2Y6CNCsqIgSx2luYHZ2oRl0I9ityzJa4U69Ph2Pt+4FQqrCeFyrpBfXOB7fuMnoSveXO8mJr4cL6BbJUD0NT5gcnB3uHu8PhQDDPRiPB7OlwdXH14rnzT196eq17TpYoSnHwYI9qsrnVzkzpGRwfnKRJXBU5hpK7vhRshtN4NElms8KWajUUgccTOz09s8YWpnBNKep+OUpNnOZZ9sbbby1eXXE15NwFzouH4/FkbGrWNJlL65XKZsVwOrIasa1HyVnklMvyOIuPZydipZYVeS/sKMPWuYPT4+Vrm7NqSmmx5HWl8u49vGsXVOabdDa70DoX+MGd7ftmkY3HpSmWZb9fb98/2j2pJkqoumuN9qeGLRJaa6WU7U7rk64QEdUnlfvHQaMDAGvtQrc9nY6FL59/5tlmYC+utP7z/+y/eXD7JIrgP/yPi6vP/voLzz6fJPHbb7//rd/+naqs3nrjl9eevFbzwvVWL9Iy0PLGL392evtubaEXBLXexpar2JX84Hj3Zz/+yVtvvNFo1F966aVrLzwDAj3PX9/aSs5Orzz1zOs3Pjg6OUEJTzz3zHQ2HWV51Ok2ltdvvfOLxXOrJ5OTZqslpbBRjWvNSZ7I/qLVgdbSWI5dWhbWqihcXre+by3nkYqLvKwcSDHMc+/c8miaMPBir5vkWVIRGqqtre2fHkQ6uHL54uhsQkLcf7g/GsXnlte+9du/u764KIRCVM8/9SwASlQoxVy9EXAQBM46RySVAHZaqx/94LsJNPJkMolnmxefaPkLjVZ9Z/vgcHC7FjTKsoK2qGxZIh2Pj3c/epAVGfjggGdp8taHv2i6erPVdsKV6Mpk1AyWHj58kCWpX/cWl9dPzclJMbx96zYUlrmStYAsT0xxcLx3sn1qcyMDPwj8OE5H8ezwzgMjKOrUEpMw8cib3rx1Ky2zeqfpaa+ssonj9+/c0ULKhleoqS1p6JK3P7gRidBvBhygIM5deWvno3gYUzLvw8NxNmMmAWLeRfLXIQSP22b+SqAopbXWOZcVuXR4vr+01usJFAXLQe55y9dKQff38mc/75SvesHC9v2Hx/v7y4tLgVJKyJr0yrNZR7dG4+N33vrlmg4kO2fKt3755mut3tH+/vb2dq0RffYLL55bXl7qtlwyvnrt2vHhXiuqm6g1yMwXf/2rQNaagoSHiAqZUJSmevrKpU63kRSF48JTYmVjYxLbUOlO5BuGrKp8Twf9bpxnxibNmpxlY6oKw9zwNAguK1OawpNYq4VZXiRJwkjEtn6MfgcAACAASURBVCxzD7lXa8uqvPf2L6OF5SLP97bvSwj+0T/8x41ag8gSA5mSpVDad0wahdZKIAopiZnIAbOpKmZ3ejRUzAGIh3fvTKp4Aatmt3l0fDAZn1Y7g/XVTdCiRFMVcWzc8X5SnsQrqysYeaWxKIQFc/rg/uXLV4NOo9VvPDi6u2MSs5d7Qp+/sLnQbQ32D86mw9uT2LPab3hOqloUnk32d0YHItatVuj86ujsyA+b79+7JU/yTq9NAVWmCL3GMEnO9valkt21xXtn+5M4yU36YLQtDJuIS1NyZqdc3p3cD21U6iqvShIgUewPD4u9JA5KarFRrrLG1x4x2erTqHqcDf1VSefxb51zRKS1brVah7MDYp7MRsbZIGj9vT/4w3/17T/zvfDVL72cpFVZQegpT6vXf/qDS+cvZLNJWZZxlV/sr/itTsiOpG/n3fFSjkej4WxSCn7ulc/oUB3sPtxcWRod7QAJS4DSDKeH3ZW1ZrcXBWL75kf72/cuPf2iUPrN13904doTtXbPc8VbP333yeefL4pif3u7v7QY9ZaqdHLn1s2Xf+1LZVYd7T/odjpLq5vZdHrrjTeuvvwiibI6GXj1ul+rUeU6qD96772nPvPZs6IqT85a/bbSKpJi9+attaeebfeX/Y2LD/f2hPYvX702PJhGUc3TQZ5nbKvb77+/ubnRXli0ADs7D7TWSmvrbJEXaZbmWZGk8XQ6Pj04eOP1vxSeeDB6SAv+0Jzeuv8uMTkwSVEcD4erF1dmqiRXELvJdGSnebPXU72as4kjiou0LPPxeBw0+HD3uBLl3mgkJxBx2Gw2b957r5ImK/LDs0wbfzVaqsjEZ+Mkjw/OEi/RUX3VsEni49Pdm3rGwRD8UFWBNVTtHz7cL7aXJvVOrT3zihN9OM3GvlODeKRLKc4p51xRlbYqx0V9HKe1lVASCARgGKXj/Dhtr7bAAYDwfT+1MQCoj63rr3c3fLofyzlnjNNaae0r0NrX0kAl7N7Z8HS2+s3f/PxrnzuvQBqBH+1Pj05OVlfXglr0xLXnXnzhhdz9RDgKMZgcj2uqe3Y6XlvduLK2UW/U/GYtFhKkJWEzk3rS91vNvekYtCdkkCBD2KrVelrXpfCVDhbXt1q9ZdQBMLz0ym+glEEUJuRfejKqNfqWptdf/FycxJ7veap77aXPlgQyCi8+8+x4NAubLaG851/98iQfG4Ct688cnw2SOAFmrofPvfqlw8kgMeXFp66Np5M0Tf1W6+oLLw1m49HRkIQGdgLU7Xv3FhsL77399kJ/8Ze//MVit/v+O+8cbW+WxDtHR9t7h4ColZo31FtrK1saWzFzGk93Dg+G1aC2VveVLqq8NDkIZLZxmSf7B531XoWGLTNRanOXlwfHJ6sbW6ayJCgxsSOzf3R05XK3ImsMzfLYJVWD6i4XFMmKTFpmnFs5yTBC2YoIDNuqqKAaV9vy2LvSLE1pqBTCi7N873iANQ3CGk5D4cdJliZO+0FVK9mAJ3xb0XA4Cf2a7bvKWemEQO9sfJZ5AQSOmRD9tCzycQq+xhYyOdRoyeZ57quAEDzP+6R4f5RueAy0X9WllVJK12q1JMssOXCWNN8/fBiFtbVuK5RQFLOE1Yfb+2ke97l7/flrMhT/6/f/wveCzY317Yd3ts5fkCqw1pLQNx48iOOp1Gp5bbXVbC30F6uqAsCa9NutxuHOA+WFAaqyLJtN3W7Xp2n6wXvvO2NsZa1xtqr2Hu4t9JesIQBw5BCRmKJ6DcWjFnIiqtciApOaZHlrpdgret1evRaFql0y7Z8cF1QadJ72E2vTKjHOKO3vT8amqsDTkziLQXhliaeDcH2lMBQFulX3BoOjd99+K56le/uH5KjI8we7J9L3KufyrHT20YmPX9VYhQZmVLXu8hIwQR0DPwRCIEBkyxSXFaQ0y/IKEVE65yxDkueQyF5VAkJZFpWtyqosU9ObjKnO5IgcF7mhLFGzsNapC2AqKyV1meej0WgR60yQ50XLb0zTSQbV8lbdKqwKbqjaWTbLqtnCZh/Jq0qoy1qal3k6qjXr4DNaCHxPgoiTpDyroIlgrXbK5lWSxAUY7nsulIBWO0oGp6mt6ms9YdgJ65isNQLkXI7/DRrrU3ms+eWj5vmaDxN19fyLk+RMoxgf7E72w40Ll9Jxejw4aS2tRvWWI5kURWnKQTYOuXZWTJ/53Mvf+96PLly6PIhnS/2FWqNZGdfqttdWNyO/fnp2erB/4HmSqTLdrk3HSXwmlcmSEZtyMJrtHQwubl7otFrT0chWZa/dWQiaP/nZz4GUVD7K+ckZRhxIqQDYGOP7viO7s3P/m7/zG33VWOov+VEURNGEkpkZlZNEIl/ZvMjs+aEPzElFaTFhJI9cFadLaxsHcbKyvKZXVndGI0JnnN26uPHu5IPbD247I5hAatVu9PPcxtNpnMyyNAVAz9POOQQEBKV93/eV0vXAu3Dl6Td3C4oAUTgH1jkfRJqlmcvT2exkchLW6gpUUWSzdDaajko0y2WOPlhj4yydTibSiOV45kchCkzydJrFMCWV+qENHbqsKqssz2dpq+a61lpyRVVBlp5Nx57zF/gcABlTJnY2zWICbpk2EFhymS3LKpvEI51rnwUyx2niYj6dTdpBLYCaIScsFM4OkmmEpiP7AEAAFbvjdNqREGEPABAFMAshnHMokD6uW38yj/U3FKFNVTnnBtYOxmeBF+SpkegJLhu+V1TZOB6068obVWk89aOm1vr6089Ox9OF/noUhd1e++HDh6995WsPH+6A0Lv7B57SjshZGp2NFhaW0klS0x472/VRFeOrl5/rLK6OZzO3uLW983B7Z7fd7Hz+s5+Tjqsyf/+9G6FWkpksVGWutEUhENBaA4+OLrFztswzT2skqOuGb7zZ/vQzr1wjxJpsjA73z194MimKo6PkbDyJwsBWxdNXLmaTkU1LoWSvVndZTKY6HA+kVIVxgFCxrTXrFy5fnpzOlnurtjBlVRlyR0fHVFJeZcPJwDjbaDbLqrRkwshXTguL2ldL9dWN82uip9+6+54hU5auslagmmazlNNC0c7J/npnra4a02Scuaz0jMAyTZJQBnmeOaRUVj7rvCqU07M8Scs8h5zQlmCM49LavHAs1RRLjTV2orKcZplwkhQndmar0qKNswkyo8I4nVV5JSOvLEowsTPVLJ3WbRttkOVZ6UycFdN8qowAp8qichbO0uksmVaebUAXrQJhysLkYFw2OUcsifSjjLfP5BAQ4VEHKX/iGP2nNRYAOOsqU5VFufNg5/yVLV1Tt3d3C5durm4Oxsn6otgeDhOGc8u9o9PDswcHtaDBjAggAYYxlLaSnjh/aSM4UoMzQETKHGs6nZ5exisLS/1GGMZnA54el3F29+49eXC2tLJ8Nh5s7+3X6/Wj4+N/8a/+VZnmaRzfuXX7cH//yqUreV5qrYSYH1EEqeSj3ixigEdnmxDEn/35vzG22tjcevPm+61Ou9VplVze3X7Q7a+QVZLFc08//6///E/PP3GFe4sKUAgZ1IPB0a4kqlVFWlb1disx4Igqazv91srS0tnx2axIe93u2tpab6E/nU6tsUWeng4H156+fu/+h8PpKDel5ynta6lVWKvdObxrBNWiKC9SIrDWlMhZlRtNJmD2oLTGIzNNU6vIBOR7ZGzlky6NISTyWdb9gim0OImzwhiUtHRuQXrSgYvTVCoZ1Hx/c6nVaXBlyjxjY5NqFi5ExJ4UMi1TY10KVnZDVIXzReVMUhapMwRGd3wOsXQmNyXYsiqcaEpoSyAGspVxh7MDbILX9R2BRNbOj21itFUNDYqYeVYmBm3lrHzUFCf/eoL0r+Sx5keLnHPzHq5+pw8EnvJatR5XapqasNmcTU4lirDeklb26r3x5CCJz4TyGNF3enf7YejXwlZjd+9+KdL1K+emk8mi12032+PxeO/o4frq+jRJ3nz33Td/+sMoiC5de2Z163y90zodDMOgUeTlK6+85gzdunlzmheoA+GFhSMSQFIQAtD8aLWYn5l8fBbKkXMIJHCSxMfv3Qgf1K4+cbXT7ZiqWF/sdANvPCnazWh8empmsZlMzz66/8LLrxCqvYNbSCZQfrPXCa0b5QUJo1AYclLYkisVVI1m4avp9p29pdULvVYrCAKUfDbqRnVvsXnJCTEryzu7u5NJrIOQlIyLbBJPHFIt9FF41plZkadVGS00z1/q1uuNkqppkc2KhAK48PTF5fYCgcurcjgZZ1W+srnUqHVkQ8cmGycTo1znXKcVdnXdq2wxS2dJEQsN7eVOFAaW7SyZMluvFpy/sIVOAkCe50CMqoyWW6q3iCFmZcaOPQ9gWUKldVMbVxTOeQr9vi8KrPUia21mq9CTwYIutVc/12ScdxxBbbG+9vRqb2WRFYFEVMDOKCJU2hF9ClR/sysEAClACilQDM8mWxfWnQLh6MUrL0Rh+6Nbdzu15vrm5uu/+Mu/+Ol3v/V3fzcU9e/+8Adf/8Y3pObZbBhozWDvPvywVqv5Jmw2GrYyga+1h+12yxgXNZq+X//Cr/26F9Sssc8993yr1775wft1LRbXzjU7C3lVvfP+O+Rc4MkokleeOB/6QbWfKgBn7DzNNj855Rw9oqt5AxnzQq/3zDPX/War2Wl1+r3RcDQbnKy36uVocHoa37x9JwhCP/CU9jcvXBqMBw5ooVFPMplIsRtPBQhTVigZ54fjQRC6dqSM091W1wS1s8lBq9lMDSRJ4rcbBrKqiJv9fiVtLVCp50V+yJJJQLvZKlxZ2syYihCSNGXmdqvdWeyXRYEC0zwxbOv1Zrffq8rKMWVxXpaVJ73F5UUhlGObpXlZFkrqztKiFNoSmLKM05jALa6sXN24fmf7dmnKcZEawNVO99knnr+0+cQsm/0v3/nnACLw/VanTgSWbZnMpHORClsbPXDKMk2mE3Ksw2h5cREsVeSSNLa2kl6je26hv6ZLS845qZVFFpHsX1jRUjGjNaw90YzqcZbY+RFv/nRI+MgVfpK0hBBaqaqqQKJw1ez08PoLLz335DNJkp4OBmtLy7M4/uXrb9x8/+bT16/b0tx6/1YjrP/4+z8qyV578mqnvZwm6frSZpGXUbPR9Bpep+asaUb1UTFRAtphp7XYIcerSxtExEizbDI8G65uruze+8BvLUyS8h/8/b/vK4+q4vbN91dXV49OTv+nf/bPs6wABiHF3PE5YmYgR0xWa02WPJRf+eKvnzu3zIhXr10La/WTk9Mf/fi7OVjlh1FbLSwtb25snA7PSgbjBUEtqAdenhWTZN/YpIWMSsykMSyBmZiANQtRa7dLUYtR5p7HkTecjNDX3XMbJivrft0utW2VNKVoBt6RmxpnBYIji4ihDpQScTqbxeM0iQPPW+z2Iz9CwsFkOIsTH4N2rR0GocFqNh3P0gSE7Lb79bBeVlWZZ0k89RFb9VazVi/LsiiLUTwkcsvNtZefeu3O/TuZycezs9KaVtD+8vNf+8Izv1bmZUO0vvHZ3/7e29+xylZEjmxSpKWphCf9Wt0QSAl5llfGSelJFebWShQ52dxZVB4qrzAmDDSgm/cfC2AiRCEJFLLQKBmBpbDMWnvO/ZWQ8N/Z3UBERVEwcxiGL3/uxSo/3b1/7zOLa81+sx01f/SzHxd5zsS/+bXf+K2vf8Ma88JTz/73/+P/UDn33PNPff8H37n21OVrV56u+0EzaAgUw7MzZmYWybS0pas3anmes4EwDJ0zf/Inf1IUJUte6Hd3T0Ze0Dg5G3W6Cz/6yU9ajUYg1Y233tn5l3/iB34cJ0rpeZfjPCqZ9zXOv+hBKUVEjul73/++7+vFpSVDnGTpLJnt7e9PsuLK5SdAelpiEARVWRKRY5CersDlrky48iWEOhycDvRCy1Tm0TcjOAuEZ0UpPTWeDRlVv9cdTdOFpaX9yVFfhaNRUno6YM4Hw3pYIzoprfXEx42TDIEKrO+yPG/VRaPeiPw6svR16OswUqbRqi00O1JI48lUJoHUzU5tsdvXwleBl6WZB7peqy32lqTwhFZ5UnkY1euNL734lWQ0s6aE0ggLLa/54uUXn9p8ukxL64wpyrZsvXr9tbfuv51UcV7lpqyUlEEQCfCAoKryKkslYC1sesJjy4UtiyoTLJUOpIocYZKnzpDUSisNKOaBuCEnUfhSWXLK9wjAmAoYiOdn8eGTWFL8174QC0EwkKlsVVRaU1mZN15/3Q8CY2yZ5UoIT6pWo/X+u+95Su3s7i4t9KJWfePc1u/9nT/84Q9/cO2y3n6489KzL0RB+OG77927e+/JZ56prL1x483f+a3fDjxvOhkJ2RHIAuiLr32+1e788Ic/2Nzaanc6o9FoZ3eHYHS4v3/79p3xaJLO4meffgYMC8FKSAfzA3AoAKRS1rLQGgAYuQI3TuKQgsn29vbJsfQUINTrte7iovK8euB9VJS7B3vSU4wAbA4Otx0YZqcVKO1TrdkJawdnJ1LC/JiQkM4SaUJnTM33Emuzwp1//rmDk2MJAdaagSeHgyMr9dKVS7c+uuscoSNySintnCEgduwrr9daAEYhhEYFjIiiX+91gpYUwhcSmaXw+s1uq1ZHQI1ao2LgheZCJ+pKISRqpP+NvffqsSzLzsTWWnvvc871JmxGZERG+kpXPruru9lDN91DMxRGAjgPEiBAAgVBT9KDIL3pkdBfGAiQBAgYzABDJ5ESSQ05TXazq7qqy2SlN5GR4f319x6391p62PdGRmUZUiMIGklzgIy89/h7ztp7uW99iwh4qjI9VZpaXliqRMWNzTWNOFubLQWFYlR+98q3DQYImgw4GRlj5qMz1bCe20wXQAMBgQAYUQBgVAiBC7VopQm0E0AEQybQASmthJABlRYliAQWUCtAAFYCIiRpljnFZIzSGhBAxmW08GVoMpxK6YhIbi0h5bk1Btm5/f3NTl+azSlrbW800KHJhT/7/M65xbOFKHr85MHW3tal61e2d7eUDqq1epLmAHjp/MUsTspBdGH53D/69d/44GcfbNea3739ntKq1+l6jpBCEN757N6tG9d5OAzSztaTjfr82Zuv39JaN5vNzNlOu/Xi2XNtVJJmVsjmrLXxbAgAKM6dmI1EBIw2zxWVLl69fO7ihdzaF8+fJzbXipDw+vUbU1PTU816q9spVgq7+1tKoWNHBCzgLKed42Q4CjU41D6xSowaVNkUOp3+1OwMd7okaed4HzkDZeOsO0wcgVOk4rjPIpkIs9MUZM4BCqEnQFGhQRHQWiESs0MiJE1RIMKAKCAIYFRRq6JjZ5QCZkUE2ig0IqJICYgiImWmG9PXL9/8/P4dK5aZFVGtWFtZvDDdnBGBjLP1/Rdra6sIsHL+fLPZPB4eawoKoVhhO8kWK6VImUCNuZkQQWsUCXylnSJEItAE7IiICEEhIgohEfjYgkiOmpxLTFAEQBxTpHwxjgVfWhQpn7UOgsBmUZpmgyQ7Uz5LeRYRg6JkOPzJBz+NfvVX56Zn17d2dg72M4U/OHdpd3evVCoxMwLsHx4oAUFM88w556wV646Pj6JCwZf9I+JoNMosf/jhz4Bd7qBSaYCTANWoP7x28fKZZuPF82f/8T/+x3F/+Cd/8mejJHFORDxHilPKeAYfD9sIghCFvvveexcvnmeBC5cu1Rr1hZnZv/ng/SiKRqPk8KiFqLNUfDmG9yadY1Lj2btWrkFssWgGSTK2Q1ERYCdNq4vL+0cHgFwrFLcfr166drXd69Vy1el1i1N1Ejxe35op1RFQIRL4zDQjoLUOAZCQCJgF0TELouCYgglF2DmLiCLAAkTaPz3PYjWmH5sYxWEQvXv9W8651qDjWHz1fcVUb6zcDIKCc/bB0zt3nn/ikxPdvNNsTiOAML60c2RctnTC3QXgWSMEUfmCZmF0Y86mMVcUOz6hv/JUOszg3JjmSY1D1q8ur3qFiBhEgc9Dd0cxWCWm4Ez++MVqFBVBYGZmdmpqdvXZ2t0799dK65Vq83sXr3b63VAZIi4WDOf50e7h7//z35uemn327Fmv1//oZx9vbe6K0I9/9JPZ+blWuz0zPatQ/eAH/2Aw6OXW1urNnYODURwvnTn7YvX5e++8bbS6dHYxdBazfNBui7MoEAaBIBhtBIRIFIG1DkAAxOYZsO11u0mS9rv9Wzdu2TirVKuZdWkcV6ulqFDa39m0ebq5u/HatSsgKjAFEwVI0O93meQoT7Fa6oz6Cjz5F4FgDpKzG3T3GXNEyZ189/u/uLW/l7GKpmbOr5x/8nw11Pqtt75998lTJaJINIhGJYAWWKGfUsfsaQIg4ABQgFmYiKxjIBIAJAw86RQrEQFCQc+ohUzAzAbU9Qs3FxaWPr33iYjTGp0VIJqemluYW84dr21t3Vv9XJQzmqzj4/g4O879pAikCUEBKkTPWuPpYgBAgF3uvOb3BDgAAs4pITfmkCIFQkgkokhpUgLCgAEqsSIkgnzi+p2YVV/AY53IVm5zZmZhhFCXS9e+de5ynuSZjcJiaAphEGmt5+YWnHPD4TAwJghDB3LY3ezFR5cWr3KGTx8/2XqxpdEorQHkn/yT/w5AEPH+vQdIqI25fOVKGAaDZLS2vWUUasUlzETZg4NtDNS/+KM/bLWOWsfHrdbx+tqLm9eup6OUyCChAIsCYXHshASRlAqIEICQzMef3fvws88LpfCTh/cq5XIQhCA8WypWq6X2/m7nYP/2r//a09VH3XabyETFkpXcibWuQ8L9Ud/T2JEmYRYAgESJVFSQdkZRs3oQ9zvZ6N7mauLyRGHaO3Tdfcsp5dnoYPMw6TF6GxcYAJA81RshsifeOMUXhx4zAKAgsNYhIoEn8nMiIP7dMAN4ghZSrGbL02/eeMcY0+ocKyQGQG1AcGXlfLFSOuwcffLoE0R+4+IbN268+aMf/2i99aIzaoFmFgFGJEUinv5CEEn76izv7JEvzfKkX2M+OUTwPHgiqEgpEhFBZYEcg1XgkFETAOS5DcMx1d03pXRgkihUSr148SSaKuV72Xdvf5cdvHi+vra5ceXKldXV1U8//viHP/xhmqbDeLSzs9Pqdb/1/bdyHm7tvzi/eK3T7WqlFSqlFAIIjAsjhUVE0ix7+my10axm+Yh0rhUxpC9ePJ6entcmWDp7tlCI1jfXt4/3U2SrcJRnLBwZypydlFUSAIIgAHmXMLcud7kmo7TuDHqskBVenJ+bbdS5fzTbONcZ5cPh4Gfvf7CzubV8bhFJtduHqcujQqSIBFgholJ+poJxoSySYDEsFmqaldY8KgUGMluMCr3RoBlEyTB2SiuAyDIWiw+EmcmxAIki8jQF7JwXLC9SY+3DMM6BgKfn85yczAhEiOzZEn0OTgjAUPDOzdu1YmOQ9IbDIaFCUs650ITnzq4Awf2n9/tJWxt95cL1s9PLVy9d3/j5FosTQgTQBNZ7DZPqkrF8MwIJTOjdvKnKYj2Xk49Ce/nL81wpxc4RIgiydczMzr1UoBPj/aUqPBGxl6uMEWZr7WCQzJyZ/5Vf+sVGrZEmWZGiIDBpEhdC85/9zu9MNaeIME6Sjc3NR8+eNCv1tR3bitvF431DKifSShtj8jwHEU8RSUSCSEShCUXowoWLn36wmbi8sLJy/d33nj555pLO+tZuGIXi+MqFS71W69alK8UgfPjwMQIbY2D8CBBJIxE7JkZxIAKZTZZnzr77rdvOuSAMTBQled7u9y+dXVnd3MzZ3Pn87mef39WhyTInmM3PzlOge4NBkgwYRHnPBj03qn8mZNkdxaOwEPV7fRFVBENIpagcj5JQh6Acl3QWJy7NG2G5AAoBUBGDAAh5yi+FMFEQSikkL08ICDa3qMiTq4JCCyKI5MmDWFARAQgACU1Vpi+sXCZSx/3jEY/QKGctkapU6pVKbf9gf3X9sVIoiJ8+vuMUPdt+LoTsUGntybpIGLXxZhIReZIwH63U2jh2ThwQisiEThGZ/PxKMAFxKAIE1qRJgTaKc8ecIZJM5PJkhkJE/YqgAYDfj5lnm/V6MUSXrz5bbbVa7Xa7UCl1Op352fnXXru2s729vr6ORM+ePtvd3S3OFCwAgljnFBIzo0YRIaVAXmJLcmZAdI6tcwcHh8vnL+wf7HdHSavdb8zOh0Ew1Wheu3RlNBgq4M8++nBuZibP3NqT53uHRyaMRDwx7njuBkEUBASj9aVzF9579/Zbt97c3dt58+23hTAIzOd3Pun32jNz86NEFhcWr11/7eD4mC1v7+41m3NaQrB9lzMazIUBhdkSMHp+MAAkSDmLezELC8GeTRqV6m6/nbksT/qmaHqdtihs1qqHh60YnBFnrFNaKSSGEwLfsa6xzik9Rmf4oSEISCTCAWoUcOy89wQEAEhkEFgJXlq5XAxL1rrO8RFLToqQEBmnmjOs6MN7HyUuzZwjkrWdpxv7z3MRANG++luRE2QrSis/VZJSXoEgKUDNIgg6GBt14rQFz/8m3maSiXYk/wWIcnAYadLasRB9I4L09OKRa8xcjMJA0ajfW1tb3draFoCgWATE/f39O3c+c9aura3t7O6awCRJ0ut0gTkwWpMaxaM0TcFjoh0jeYY67ViyLDOBOW63mkGTMW9Uo4Xl83GaE0GaZ2eXlwKtPvnso9FgWC4WHzx/9id//mfpKP38wf3eYMgMjp0iQkKtlPMuBgsAKqQoMIfHBz/58Y8LhWhjeysqlRAkS/suHd6amuZAmlON/b19J1wKC/Mzsz//6P3Fs/NohNCKsNfViEgTAlxSSgkYrQO2LqROGhNYmw41OFFEIOCsJkKlFGCo9Hik0zjODPSSydjHdZlZrHj6Am/fsLAnYBYRFCAkJGBhQGAWYSbEyITnls8DE4gbJTESsTApUqQW5hd2Dra2OptWOScohIrQgrXOkiIAhTg2HWCMGxuTJzgRCiP+xgAAIABJREFU5ywQKYXixLsJnoWXPbkooSZvMJEnyYVTqWTrnBNGpaJQiwgpOi08r0beT3u2zjmlVKvXtUHWjntxnOvIsMiz1WdXrl3rdFp/+aO/vnblSqlYrpTK27s7hVrlwvLFo0cHpVLJEB4c7wcUSsJ37txxzIioTRCGZnllqVKrV6emTKQFeHdnq7MnSwtLB0edxvz8k9WnQSGMQqORB3a0v986iHs7/dbzZ6upyq0S53jxzAIAFAoRAluXxXG8f3gUBIHNXWKlMxwmzs3MTH324H6xWIoKhXq9UqvU3v/407kzi3cf3sszq0nXa804785MVZ48+nzpwpIpGAuZBnI+c6EQCB0z5w4Fg6iQD0a1pblenDZN0NvYa8zPDkgaprj1ZHXxyoVRksRbB1PFSiAkAlopAAFhJCKlAQDQk1pjYJSn2faBBqU0wXiWIqXGisLbdqQAcwRGpkKh1Kw2FSjQAEQijFoDOwSabjTvP7tvIQNN4gAIhQgYACySCBIz+6FIJ/wJShEhAwv6uJQFtoggCj1FGQGJAOJ4XMCEJwYRx9S0CEYprZFBbG5FhB1/deT99CovVeMQqjIjJT/59K8r0VSjNt3vD0UbMtHy0vn3X/zs7r1n1XK538uqlWnShoEAoVItV4uV5kzz2uVr7eP2KBmcO3fu0cNH1iblcvHypXPTcwtnFs8CW8mypNNXkMdhsX10nBGUCqVCEFUaFcdZPTQLxdKZs4u3bl7/4z/+49EgBkfpIL312q3Nrc0z82cKhTC3dmPzBcsDa22z2QSE27ffvXj5UqlaIqXKYaFSrS2fO3fns8/ifPvaW+9MLV2Q3GqFpXLhX/75n1Kbl5Yuvlh7trRyLohCUayIQJiJwLGzzERM0LGDuXPz262jnF2X+cZ77+22juL2sRZ37Xvvbe7tJOwu3bxx3GplKKFWztmJdiBPwG2dI/KEj+DYaa0AERhzm2s9to69eDGzJh+XsALMIgSqVCobY4jRMTarTR3oHK0gKK2Vpt39XQRSIKFiQhFAVkTeYhQZC4IiH62yCCDiwx2A6EQUTkCwAMCC4snl2Rt73osEEeccIfIk/AYggk5r5QMipNTXRt5PL0Tkje7hYDQzP/trP/hHlWIly1y5VDWk93f3zi0vn1lYYuvSYUwEhUIBiXqjg6XGVAHAufzihYsLC4v1Sk0rdfHSpeEoTvLuysrKretvAuDyynllVJJmMzMLJYKFxaUYwunFs6VqVUAci9ElhMBZUSosRNXf/I1/dzgY9lrtbJSdmVs4ah2fXV4MA8NORHhqujk3N3dweJhmabFUqpWrhUJpkMatdu/Grbe0UpevvNZPko1nz2emZ8Nq9cmzJz/94Emn1xfIc7FLy5d6rd6br79WqkZ7e9vDUddoCMi02aVOCkZplP3j/ZHLAMU6uLv2NGdn2dlkONhZz/McCDf2d0ZZRgDWujA0MHmkY4ptEQDPpuTZY4U0ESGzMI8NZK8i/AumsdMORAodZlnmHBsEUnL10rWj7uHO4Xqc5dVKoztMevGQUQhOzmOBkMg7e44QBdF5h9zTg5NynmGMfPrDGaMd88v5ZSIZ1uP3EJAAERhAoQIgALIiHCjOLSk91vtfNKXgdOT9NIjUo5gXzs43ylNFKFeDSjfprz18XCwXr12/9qd//md3Pr3zW7/+m3MzM51u56NP38/ZvvfOG7PFqacPH711e7FerTUa9Xq1Fo+SQlQoViqlQJ2/slKqlIDRBFqFoUVYPLdy59NPHqxt3Lz5erVSi7N0FMdaYZYnWmtS2uZWGMIgkCLbNHdZ/7jVXlg8szBXmZ2tDAYJw8zGlhuORivnl2u1+nS9emF52Vq33T3Oms3f/9P/DQEKhcLFc0tz1bCgZZS1ludqq6vknNWKhoNk2Et/9TvfKRejNMsTHZlQbNY/2NuJFqdSm9dMcePx48WrlweDFgOUFBZzVkqPgAtRkHQHpUopt3lk8wAVOiQz9i0VEZECAmbxrj1M1jrnUNCoQJNx7ABQaQUwznyDAHhlJGCUsrkdDobD4bBUKaFQLaz+5vd+izHfbR/defj5wdG+I2bHpEjEiEBuLaJDREFgEfIs3yQ4FnD00xICAQt4unwW8O0OvOU3piUTxaAUIaETJlJe3nxkS0QEBZVy1nl1+QrbDPg41sn0dSJu3jduNprxcPjGzVvNWn04HDZKpQ8++vCgUgsY/6N//z84u7CgAKeqVXF27/holPH0wgVTbGSMOirstY4UqqhWkkBNL8wMkkNTDHNtLbt20moUZrTBYdqPygXQJAYyl1iXXVpZ0k72Dw+ctSYIKvNzD548ikrhKO2kdlipV+fmF1uH0a//+vfq1TID/8Ef/8uoUu92u07SmenarRsXL66cRYDk/ePN4/7u9ka93qhUiqNh5/zN69PNKiD/xV+8f+Xi8qDfLVfKxpilheXpWunypUVB9ckn6daRS6kwdWHlsH9M5Dqj7so7Nw8OjgqoRCSSYLZQEZB+3FKiq4WqmCBNbbFYdcxAAigo/n9wzilUMmHL8/rGWucVZZZmxmhvydjcCYBzlkgBSG4zpRQKsBUWGaSDv/zZX3z7je/NNeYRbJ7la1svfn7/o17cpQiRBBicOPZJHkRAdON0KoAH2Y4JktEKk0/2WYeASGAZxlSRHkGJKFYIUWAcokVGQWAUEFEKncuVUiLaaKW0QbBaaR8GPxEpOQ1NPj1jeWVvjOEsDVW+9WL12TBzIqnNpxpTO1tbs1MzS4uLneNWFIRbW1uG9Kg/eDpozy8u6iD62ft/Y/O8VC4JS71WW91+wSS3br07NzeHASmlxYEOIhVSuVwtV2r+iiYKBkn/yeMHZ2pT9ahQr9dR4ebhbrfbqqj6MBl0Bx10cX1qxoQhGQMiRBiGYRDJQqW5vbUugvVaDdACSKlczHZHzXp9enoqjWMpF7UClEQATagBnDY6CAKtjUya3hC4UrGUc7sdD1KbRIEuxEIBbe7uVMKCaifFqfruqAdGJTaPnbVJUi2Wur2eBTZskyR1YhFDNCEpJb4XDiijMXOZTzywjJ1zIlLK28bjfirCHARBnlvvZU76XAARAcrG8fr+j/bLlUaxVO52u91ey0oCILY38T0JfGcdJgBC52BiDAERAikEdOyUMI1nD3XSV+ZlNMHLBCISKlQCdrIDAIDW2v8KpZRSygdZgQjJR9y+sOCXS+xFJE1TAJiZmbl06cL65t1O62Bj62iUJCYIkzwtV0oicO/efZflrcOjLMtmFubjOHnt9SsHxwc7z5/oUK5cPk+ktCnUKhVmUBQ2a1MiYpGRVKSCfr9v2UZBAIC5y43RO/sH2kjraC85PqzXattbggL7x62cc6jVBr34cL/1vfe+Xy6XsFhYXd1aXqjv7h0dHI5SKVXrtas3XidIHz1Zu3Z5pT/s3Xu62U0pLBWQKAwipODJ053L52ePO+3do/7OUS+3LgqjWr3OzBtbe6WyZpvfffi4MHdGBnsiqFjPNqYEIe4cBxRWK+GQyACG1iFKCmw0ik2IWBMqcSVtRIEgIWlQSsCNk82AihAmwFci7WtawiCY9E4So5VzolCJErZOkyYhy1YIxgz/CLEdDNp9e8zemQMRr84EhZmJsWCKxah02DnwbhoiOs/XDyDMDgB5TGyKAI5FHAujIx8RFvbWkxCjYwEWN0lDgTBrJN8PyRNai7OsHGnMEyvMJ1i/0ymdicoc25jjzczc6/X2jw4dBLtHrc6gExa043xza2tpeaXb7X3y+Z3OoFeolBOb//SDD4ZpjAh5Ojrc23j32gWVdSMj9ZIeHKy7wXGlHDnKUh6l6Sgd9U0ARkOzXgVh/1gr5SI4C6O8f3RkB+3AxbWIGsVAuSwbJpK5uDN88+atLEl3NzY7h0eHnfyje7sbB7EqV45aGzt7T4ajwzjJ+7Fa2x0+XD1uTp2TUV7U5TAoFIuFVqs3tIWNY1zfh+m5SzP1qbffeGNuftYERlPkVHPr0H3+5DCYXtw42MvRAWHCdiPrrPX2ezJqx8MdYw/63chEOMiKCTRMaS4sHz19cabcKIqGfqqGSYihY3Y2d3nurHMuZ7bWZSxOhK3NHds0i5lzcdbZzNkcxAlbYQvCzubA4g3c3OaWXc7OirPM5VqdSQPoUlBSTAVTqJfqSmiqNlUr1whIi1mcXnz35rvloFiLKpWgGJGuF4IIpFEsGlTW5izszfUsz9myMKCgFjACigEZCRQIImtxBKxE0LFYyyJgrbPWhw2FWYStA4fkO22NIQ8nSnAcBTyZD0+sK++h9Pv9n3/8mQpqCxcuWoO9LG3Hw1/6wQ9rzanZhcXjdufuvYeZdcfdTqFUVIJbGxuBNpEJS0GRLM1Um5I7o7RGJc5mSVwuFiJNzWbtuH2Uuqzdbi3MzRaU1IrB4fbO4e7+0VH7448+y/MkyxJxNk2H1uYPHz5yFrXWrdbh0rmpyyvVQftpnh1L1m1vPq7B6NrVlaP9rft3P+n2txJov9h5OBxuF0zntYvly8vR7KyuzaizS1G//+LR+t2t1kYi3TNLtWJZTzXCxdmoUk2Oh2t7e096o4Pto2ecH5SIAhQAJpeTAKIiBY5zQE7ytL6wUJ5dsLn0kvTmd759PBpZhvLsdHl23jKyMIP4HiTGBFprY4x397wSMUYDICARBcZESukgCJVSWhtfGkJKgSaLQlr5V0OoQl3QaEqm9A9/+FvT1Znzi5fOLV5qVmZfv/ZWISjPTZ+Zmz2jlOr1+0mcXrt6q1yoL81d/M2/99slmf2tX/7txelFQwZ9KBQBCEWhKGIFAg6IEVkjiQCPu1wREWnAgHSkfa3XeFFaa619+yoW9pgI33LmZK4aG+9fDjegIlRkrW1OTz/eXJOQ3nz3W1EQJUm6/mKr1em+/e7ti5ev2Dy3WX7h2mtKawIc5f3VZ/frjTPv33n89o13czH91PUHbv9g9VvF6UJUHB11yqXo8b3702cWiASR97a3qsXC04eP4tSaIKrVZl67/jqAtrlNkoSIRNyZuTnSqAo6TeLjbqdIeGZ5uRcPR6Pe3NxMaiVL89evvZHEiQt1nHCtEG6tvwiRnHN7BwcLl64QBkjDpw/vXbrxelCrxk7yzCE5YffZxx9euvbGMBv2jluVRnOm2pCY1lefX3/7TSR9vLaaib509bbi/v33fzp7ZWV70H7R3rNAFnLNrrO/nTuHSDv9lgLN5JP8gijMTkQ7x6SUJ3gGEAQlAFqhZU6djcKIUHybQq211jrPcwEhX4DEjJ7ymqhaqjaqjbXV59vbu1lm89xFQWAtpDmTCtJ4mEsySuLN7W0Hsr65ftQ+Vkrt7m0x5Dt7W2wFGRmIxbIQCwuR89S1IOxb/QA5EBYxYAmVCAMQW6fDwLsEzOyDD4ycA4hmrVXsUjzVluK0eH1FSseTfypSnFsM7d7B0ZWFlbjX3d3dLRVL1Xrtb3761z/+q7+6fv16rVrt9UeO2Rj91ls3lAnn584tzFcLnGlyM4vN+uvv/OVf/7VlvvPZJ5h2L547s7m6HRZLoYkOD/f31lZfO7f04OGThUvXgsgogumZ6emp6tmzi1rrYrEIxelW+hgdBaa4cGa5XG4CQH16cTQaFatzJowK2tz99NNrFy/EneMzzWVdqRaKxflfvBAnw1KxFMxsP9t+AoyNcu32L/zKMEn3OvuDPEbhSBcaC8tvff+Xtrd3Y+ZLt97MODvqjwTtlTff6Gb5cNCZPns20EF/2DZE17717tGwWzSmSsZleaywHETDdne6XklsFpECK0XCFBBQgEDE+2nk2JJWDlArLcIC7AQVkXPsbO7DC57bx78Ptk6TisbUAQ4BBdzWzrrWOnWjOw8/jpN4uNMzJkiS9JP77cQmzuWI4pjZOUY4aO2Lcvvtvd6gldj053d/ltmUSBwLCqCAz/k5EUIasxl7v5GFABgIBJkREIAo81gSBBHHTL67lCMUFOcci3j9+GUp+krBQkSsVKtnZqeP4uN/57f/w8WZs71e90XjxZP1NQOSj4a//Avf+/a3v+2cc1n+wc8/osCsPX9ea1RKJbq2wDjqHe4drpy98XTj/srSOUa3vb1zfq6+vHTuk4frca9XaIT9dieJ47NLZ4/7I2301MxsEEIpq3cTl2+3QcSy6/S6z19sLiyfr5WmCqb88c8+R9LenVFaWevOr5xbWrqwtrmzvvbCmmD3wR0ruHLu/J179y++drVYwtzlxSBs1qc4tUVU87Uzh8PhUXsLKB05t7t/2Is7qMxREneG3cPj40CLKjWCsHp8fNA+iqtRrdasPt/ZQomZXZF0sVyymKV5LAxTjWkIKM7yMIrCKCAkQNBKB0GQZZnvOhmGISoSAMdsjKdm0cASaAQBhnEPW2OMtdbDnrxB4u0hbTSgxHmfUwaSYRozSu6y2CWImCSxCCs1xqKKMIBvSydOXC8doaI4HiBpx4zoW10iALKwv1uYhMuRxzBlQI0+QT7OFSKOHRFFSEoprTUqQEKttUgG+LKY4iR0BV+OY/lLIWKz0bh08Ty+SCnnx48fNRtTuzsH5UJp1BuUo+Ltd94BK5Jbm8QaOIjM7PyCCWBW9Y5frP0X//l/m8Txr/zyt//r/+a/uruVqtLUe9/9nh2O/vr9+8tL56fKhdX7H1699nqrUb+zujEQd2GugZTluRy1ju9/fNemGSBaa6NCIRNO8iQKiMTZePhrP/i1P/rDPySlfvUf/ODe3XuhwkIUlcr15eUrry1fuDLTKFWKVhePdrYi5MAUIlWuRlWd59//zuta04Onq8P1USksGlPK47xoTB4UBIlzrpkqF/IkyzQWIKGZymx/1KmVp7IUpmeWWofPrWQjx4fxQBT1bd53rlagQaeXI0uWhCrPwCFoRQoYFCoPlUMEm2U+naxJO2ZxogLjnAO23sz17X8JlWMHhE4cWwEA0sqJ2Dwn39GGxbHzsXBx456iXj05Z73rbzkndkoAAaxDZADRwgS+2ScJIDi06IQRtdJWHAoBAfk+h6hA5Sge9qaBgB0qzb43se8/6hyHKgChoFAgTLzbeFoXfgE2c9rScswscnB4uL61Y4B6rfZn9x989NHHYVi4+cbrSBjHyd1792ye3X/woN/rLp0/Fzh7ZvFsFg/Pvtb87//o9zY7mbjCn//k0X+J6VStkIWRDmlv92j7ePft80tnL14pNxtJmg3T4ZPnz8+eO4dEli0JzkzNXr5yeWtzK01SE0Vvvf32Bx986Kw8efb4jVuvR0HU7XaeP1911n7ve9+1ed5qtxrNunM5oHz6+b3jvXXOhylEplytzTY1shYITVgrh5oSZFpeaHxw52FYLNs0q1Zwf311dvFcO+5At2NzrpcLGVJ/Y/3s+RWtwmJKfLxXqk8FKrSjzDSq7VE/yG3cHRUrUUkFve3t5vzMyGYFy3YwKGnq2ty6jBQiiVIKUFgcKVRa+66IgMjMbG2oonde/04xKGY2+fjTj+MsrTVrR50jtPn8zNyV85fv3r+vjK7X6k+ePLlw9rwDfv7iRb3UeOv1N5+vPXuxtz7u2wt+rnKhji6ev/Jk7XGWpM16szPqKq2jKBoOB5lLAQQEri1fz9k92XrE4jSqki5ON2e297dJQ6PaOG4dVsr1UBciY6brzXsv7qeQCqrxJMdjlKlSmlAZE3DgBIEIdWDklFTBadjMaUiWF7Isy5LU9UYdONzt93uOeTgcPl57duv1W/FRdvdnP61UKrtHB/1Bb6dzVCyV5hcWTIjbB6O//w//vX/6ez9ut0a333lL6WBr+yBsmOZ0fTgsFUvLUSlc39xEYFOIGmfmv7O4FEaFYrE0GPTqlUqv36/NTFEYJGlaLBQLldLM/HxUKpeqzVZ78Nm9Bz/6yU+KYWRM8C9+//dqzcZr168bE7Tb7U8++VSED3d36rUakC7WkvJ0rVwp2dTu7x4uNa/GaW60vnv/6dT0/HGvEwZG6eCNt27vt9uY0qVLlweD/tGgZx2dmZ0lBEIohEWlQhdgoGRhbm4geQdHxVJ5qjG1228D0NUbN1uDnmEpFsq1mflPnm+DWD9RMbPSKk9yDw3wLpXwOMGvRKETDdGLte2oUpieO7ux8WJlaaXdbjvmWrH+/Ok6ZfriuUvD0Wh59vz8zPLO3iYKhkEhHVoNATGIg0pUmpmeXtt6bhEWZs/OVc/olXB9Y+P73/p7f/av/nTl/Hkierr6JLcpAIYmvHD2cpbn69tbIxwg0M0rN751473/8ff+hxuXb7xz681/9gf//Iff+mGe8WH34PUrr3cHg7XDNSYSZBrD/WDSOFKSdEQESutJdPRL3A0nwvQyjkX0rdu3r752pTHdONre+fG9jw62jkwUzs/Mz52dL1bCy1curb148eDxo3KlUqxVsyx57cbVxlQtibO9Y3nn5s3/9U//p81nDy9fPP9XHzwyjXPH3b3pSrke0EFrWIB8lAx29w/Ov3azWC0jaUU4SnvDuF+rFecWZpxlQQIBAkDBX2hMt9sDZ+nGzbfOrVy0LiMUhaADAkJlTDJIfvGXfuW1668DkThHAk6AFAURrq4+2946qlUbB+30/rP28dG+mJIuBlm3ldt4vXOEAN1eL0d6tLcXx8NRnlpnISokR4eWXeZstaJG7SGiAhv3beLYbY56GCOK9POsdbSdswuUHqX9A5sPcza+2zFoQGHnk3MkLN7GIqIgNMwswqnLNvfWbr95Owqizc31/c2N0TCOgijNs7BUrDYaD+89PGwfHh0dzc3NDYadYjUCsofdvfpsY/1wmwEDE96+fvvm1Vv/7H/+p4eDozzJKmGlMF0OsKBdEGJULlQiXQD37O9/59eiMPr87ue7+/vMLoiC0ZCE5fna2hvn3zxTO/MLN79vnPrOa98/37iw2Vl/8Px+EAaFsJSxEDm2ThQhIAk6ZwEUE+TsQlQg4vnJTs9Kr+KxXiahAT78+UeDUa/cLKPShXol2T9cPr9UDor379+L4/Ov33r7yuUbw5G9ev2aMeajn/+01xt98vHP41G6cGZxbm75aK/j0sZf/HxPVS9t7R1k6WDbbs/NNFoJdFefl8uVVPSjJ0+jcum43TJalUrlo6PD588eESmXO0WmVqu12+3QRMZExULt3PL887VHx63jqBgm8SCOR1Mz04VyIc5GSsn60SOFkQZzeLBXLhaDsBAnIwErwHMLTZvbveP9xNlKtZFbOTzYrFTLrd5BPGwHRAnHGfBhd0AAcZ4BUXuQWueEOSDT6Q9z50CDZmCbAwGhU4KWGREMKmRfQqgCEiLJlZBC8n2LUCnSxgQvkXa+LgYEQJzkmzub8TBtNqYLJrh0/hKiXpw92318d9Tr7/X2EahSrmrSyWiUxnGlXg11KbHZ/NzCk4dPQNTs1Nzrr71hdOG9d7//J3/1v/QHg6Pesc3sQWvvzNk5FcJha2/5zLmEhx/f/RgA0iRdXjrnXA7b1qByYIfZ8Cg57qWDf/Xpj25evrlxuBFDMtOY+vb17882pgej2ChNAFZ5sBUQoW/8TkqhMYKS5ZmiV8u/8HQxxRc3kIhbW1v7Tvre86PV/qh99ersQkP1DraWF6oA9q/+5l9Wq9HC2UJmW0DB8so0qWSU58VqvTPsf3jvzvmV5aEMhil3d18sLy22j8mYsNSsF/tptVJtNBqbGxv1eq1Wq5eiWrVasdYVw0qjWR8MBnmWl0tFrXWtWl9Zubi3t5dlaa0R5lluOag1KvfubSDo+YWFdv8QNLOm/mjUbFRIwnJ9KtRBqVjo9vtKky+6IqOqU9WD453Y1QJjdJrE8VAprKvAdtrlZq2VDJs6yOIYFYVBYFJnSoVRntZMkCVpX4vSppgJF4Juns6qgk2zPgIpFSQcRcUup1UV0DAuGJ2ByKQ2QSY1Of4DEQkwe1gfEijJJI0pWd1+hixv33oTGI/ahyBu0O9evnz52bPVje21hcXFp883L5xfyVya5iMn+Wd3PnSSEUine/SH//sf2pwtupxzJzDk4erG6jDpH394fNw9aCWtg+5+ztlBb4cQhWF180m92hgksRPHInGe/vGP/iTLsoNnB3c37tvcwsHDINBaqFSq9ro9GsO0zQm42guNQcXsUGkQ8GDjk1DWiTjpE/T7ycBShAAqHiVPHz9R80RiS2iKFIysROVo5dLlT+7e+eTjT8Og/Iu/9B1S+OzJaqGos2RUKqXNxlzr4DBQ9ODRXZunURjZLO31++fOrSTDQevoME+SYhT2eh0i3NnZadTrhwfJxubG9NTM8tL5YS/tto84t61WWylVqVT7/W4cj7I0brfbLCwABObcykpUMHkrzvIR5qIoEx71Bu0gKJxbWUjSfiUmJBbwtaA4jLuCablSkCQvFopZnktUkHxEVQVap5JOFyoWw45LIhVAPiqFpSRNyzoacF5UpkCRsmlUKNo0aZpwEOcSaiAsaCqXiqNuXDEGmUph2M4yARREATQm8M/YWlcshForAU7T1Nd3GW2A7c7+RmgMCLx/5ydZboVECrzX3dv52Q5plbPdf7TvIH+08YBBUInL0m4vAy2M0s/z3vGIRQDAQkaSffrwQwF0wsNOTApTmw/bMSIQkq+qWd17Svs6JycCjCLinGVBccw28XXPZLPcshtkAyJEYBYkz2mAZB0rRchiMy1kAw/fAsDxNPxFVXgyd52s4jE8DT7/7P7F717a63SbUzNlYVerpXa0unlveqpw+92bRhXKBVBavfXGVaUgCEypVM0yVyqW8jx57eqSh+8AwJkzNeeyJEmWl89MT80lSTw3O1OplBfOzHW73XqjPjVdT0b9/d0niDA7W6nWZmbm5judY22gUDSoojNn5iv1cpIMrRvNLVS7g/32o0OtCRgazan9g50ceaa5sH+w/9nnnyktzqWkBLyZA4hknbVPnjwUwJCC5YXFnb2d3CZKSXw8EIA0YwKM41hrXYkKBzt7qc1rKhMgAAAgAElEQVSyUQ4AcRwbNSyXynsHh5nL0+RIE/X7KSIWo8JWbydPs1E3KRRKw2EMQE7EiThhJWKCABF9YIAFEUiRttaJR9MIKSBfaeKEWZwmw6ydsAOnidh5gCYCIQk6doAakHCcUHYsAoTOOechn4iTjJzHf4qM467j2gxAEWQNKEKTDqMKRADRZ7sF0SGg0gQgZIG0QpoA5xFJ+3AVB1xQYECiMHDW0Slr6lVVeBrlRxMURLvVPdo9yCL65OmzrXa322q73EUUgpOAtM3l1g0UgAcPHzubV2qNK1eu3bt3/5233snz7NNP75FCZs6yDECiMFJajUYjADSBERFC1NokaaLHtPKCAFEUxfnIunH3Lo9OFBknzsbxEgQEH5Tz9b3sOBfWpBRLTjSuBCdCgnFROZJmn4RHAFAgnypgQAGAjC0AEJEGb2QDCToRQV8QDQCgcPLUGACRQTQon2hFAkAgrUiDMiYqREFIAJYUMNjcsjFGkBk5t7kf4EyiFAohkgTFSCll8xyICIS0ytIhEqJBZgfCipRoQARrrWXLwALk85EsIgi5dQDCwIponHUhQAUA4lyuFHhGAhrXaHjoKAF7ZL0I5B4B4ROYY/kAAESFDALClsc1Ikgk4IAQaZTzQNpbqbW+BuNltcRLr/ArjPeJqCVptvV0fe7SPBVw2Gsb7csnERgyYVFyb/W+y9gY49i1Bp2PPv85s/zNRz8NjVFRSEgaAJQJggAJmdlEVC6VvcudZzkLmKCotULKA6WU1iIC1mpNzEATqgkAEAHrbLEQoFgAsM4yoCKTW+fAIqIJA+csccgsWhPAmHFAxmUwBMKKtDCzOOdSEcXO+fJx6ywAMxKL7wlJSilnrQefiIgiD/0GAnQiaDRyJgCpzRAdEjpCCjAsBFEexFnPGE1EoDFO4iAMHXrcHWoim1vnnNa+/YdY5xTRWEpYkJDHf5kZPAwQ0AGysxZhjAMlRIWYWw9hH/tbCQv6qkh2gICkUseTt4ngw5O+Wz2SD7Fba1HcGPAu44yLTKppCcfoMQGnFKHSTBRCIe1l8ZEu2tCljoGU0vSVeCz40nJCwahIkna29emGKbPS5Jzr9WJrJU0zZ8XjipLM4gTh6u8PUXzWRSutlZ50RgVrrfMkGQj+EBnzHyMp8m+Rx+L/0uwbP0qi09OsAMikVsDvOT4XCE7KRZgZkURE+UmMBRBIEzMrpdg6YUEE0OOM76QeACa/AmTCVaf1+ME5dr5IUICVIkQEdgKgQsOKycerDACKF2VCICIL4sTzKSqfR0TyBr74O5ST8r3x+TzalAHGAoae4IHHaGD/i3yozFrrZwlBQMATbIyflphFKS2CzjlCQnETKB85xyCCvopYGMeeDjpkZiZFOHkLKDyGNpAKsVDSpcXpBSwSoihlQIDlZXT9Cymd09PVy5cnokChECcwjC0iImrMkVwOcZ7EqY9d2DyH8bE4ecduXERANh+XtY3/eICOh4ELC4wlCSaFnegjPyATegmQ8TBFHFcpj5U8ewHll3OvyLjSBBDVONMGqJRizhFoXOMPVkQYLE/Kncd6D9HHxD1Jjx/E4weC6DJhdt5UdTCexoREhBWIINo4RzgZ+R7axw78PTt/FiJCzhDYi78fCTT+XSAiSCg+mM4gHlNOSoQYHaAVAQJy4hCJwRKCCFvy+ARBBJqkhwSQAAXHZfUOrKdTYnYkXvhElFM4llUhEkYCGSNFRREZ8O9rLA/G+4SGKDCqGEWIWgSNCUEpa+1pfMwXvMLTqnB2dpaIhBkQc8EgMIIZWAjCIEtTQkWaUkqVUuIssyBpPyswM43nCd/T3BM/nQizMDMqnEw2yGN0pHfIfYWSA/TwgMlNiQD6QnNAAGFBEhDxDAI42QsJnWMExnFX1QnJC4hzPlvFkzt5WbnrhV15KwkBmfyoFXIv6wMEAMfT6uR643jU2A8i5bf6EoWJ+QqAoic34W8HgZkUjwslhPDlcPJagoR8xaoDVooYnEMmAhKFoH0RhFY0Zv0EHtMjecDeuAoDFCoCT9LEBk7KhRB8rFZNTB3USAgiHtkszMRKKQJAN/4RQgDezlVai1csWhUK5ahYI9IAlCS5MkxEYRCe1idjwYIvLrdv37569eqjhw8BwATK2sxzsLCzzuY+SIOklDKAlOe5JnTOKYXjElsnzOzP75GsXi0SEWklIsJC6Jm8vAwJETIzACGaU3gm9ql4GdfGvVSORGo8vr3hM9E8XmcAAJ2QNU7EiNkzJHhvCMavHr3KUICeZHMsb0T65CRjXfCFq7+0J0TETxFE3hjyatq/PABgX7QNY50wLnFG8ohjmNgBBCJKKefGCkijEhAt5GsrxvB1BQhIk+JEUtpjCowfGMAojEQKCdx4sp4UcjGMy+TFExWJ+Mp+8sGliYIajxwRHld0j5/DGAqqlDJKl8tlYwwj5M6SJkSMoqhQLM5Mz7wiSHi6LZh/H4cHB7/7u7+7uvo8Ho08HJWUstYSETvHzNabS8x5nvun7w8/CQyexPhPPp+W5dPXgpdaeKLmhE8k4/SBJ47FK8GRkzUnKyd//caXkjEWtsmeNHajxosaUyULKjr9o04Ohy+6zP6iDJOShPGqsYc1+eM5UCaFn3hidaCIn6vopOyB2Wtkr/b9sV5IEYEAQeE4NgEop2wA3xlj3NFUEcK4kAvduJiCPBkMEY4dQBE9EZqJkQcyIRYAGMsDkRYRY4yIeLRMKYwCE+Q2ZyBffBpGYb1e10r/J//p71y9evWkEv/kiX8B3cAi3U7n9//gDx4/fJilmQDARH14TON4vH/NcnK2V077ty040T1wEm37u5zh9D5f3P9EOLyoAXhf/GuuLSemO/3d73ksKy///+K2U6fHU/fmVaRMHiPKibxPdpexxXiy/8t59otr4OWH8W88fSY/pOGkj+7Egzx1a5OPCCccf6dE9ovG9ys/zQ/O6amp3/jN33j9jTe8Ynn5I05mlJOxPr4p5iRJvCL7t8u/Xb5uiaJI668CuL+iCk8mm5d7nGre9JVTyCsrTykj/Lqv33AsfFH9ffPO/3rXfeUq/xp39crD+fLWr/z6in7/uq/wxXni38zrfnn5oh2C+Erh/ZcP/oZz/V9c/u878/9Tt/F/6lT/377uy/Kvk1VfltnTn1+ZsV7Z58SKfWnOfs1R37AGJ8vpz19pbL2y6ZXdvvLrK7d0euj/3a/7lctXDvev3M1/+Iad/+4X/Tfquqf/fiGOdXLMaefry1f9uun36+7yy6f6hl/yzb/tKwfA193n6a/foEf+Lk/z5CSIp83wl3dy+lSvOLyvHPuVVzy9z5cv9P+W655+pF8w3v/W5/u3LnhK0b6y8uuu8nVbXznVl45lARJxfgP4GvCJgzV2omTyj0FIxhn/l46TAKBPn6HPSIsIggLyQc1TF0MAAUFBi0AiKCgoAEh44sD64Pn4qn4hAQaYMCRP0k3/P1no65TF6Q+nN33D1i+f/etWvjJVfMN883XHMiRJnv34pz91NsslE86dS50IiHWSO+eODve7/Z6T3InbWluPnRPJRYTBCrO1GYuAuJytFSdgLSd7W2uP1p4K5AC5gGXJGVIW+T+4e9MoS5LrPOzeGxGZ+daqerVX9b5Ob7MCg8FCLAQBUiQoAqQMUhQk0RYpS7TExaJ9eGwf0pZlyTLFIx0dHdLiApoCQFKkBZIAQRDbYADMgll7Zrp7el9rr/eq3p5bRNzrH1nd01PdM5whTR8fxa/Ml5EZmS9u3BsR997v8+Ltlvs6z13uXJw7b11y7cZlFg/gAJx3NmXrJXfWO85Ecmb2YFmciGfxzz7z7dTaN/hz/vM4vfULwW3CcecNdzUWr3f1rqJ219M3ee+dr1sUAQEIiLPFhaXf/a1f+eMvfSsb9n7yv/7ZnPnSpYtJzr3+5jOPf+Pywlqrl25stp9+/JvAcvXKtdjmwPZLX/ziJz/52y+/eGat2ey1e3G/t7S0Gif2U7/9WySwuLyeZfmFSze8d5vr7eWVldWFq5/7wpcE7Lcee/TXfv03rl9ZPnX6dGuj9bk//mLOfO3qQpI6APidz/y+89mnPvOZJ59+ttfvLywvdjt9tmmnP9jYXFu6emVzkNz6nLsO121X39LpW3rU/wft3iWvEO9mg/BuNm6btN1Z7S95CnebaYqIgAcBAlcpV5Nh+cb1hW9km0ePHvvsH3x2fLz+6KNPJVnb9+PS+OyF89f6rZXRkB/98h//37/3J8cf+cA//gc/eG3x2o//+E+0N5Y+9Tu/71I3Pz8SVBqt7mbHRuuXXvnTK+0aDXfu3v/Ec8/ceOlsWI0+9N3vvXL1BkM+MzPzwosXsiw9f/bCN756/fri5mNf+cKnP/3Z+97xzp/+yb8f2t6TTz116L77z598+Uu/+3unF5sf+9hfnxopn7nRatRq+8ZDgVc3pl9v3b3t7715Whhn72yT/ZAoVGYa0Nx1XnHrrjsf9ZbafRNvdfd2i6K3Xbuz9rbTO6vdtcJd7/0LnG57461hIARQsNKgoH3g8L4nnjqzd77mXGbTxATU6VvShqLy9bNPdvrZiXt2hWC+76Pff8/R4yQoNv/Sl/+sEhjnhDUolg98+Lv+4A8+u3fnxHhj/B0TR84+/5U8zyPNBw7cr2v90clxm+XMmOVwcOfE0y+fqyDnngMDitT3f/Sjh44cRZb3fed3/NN/+Wv/7t/+bxdeeGlu155HvucjDz9y9Bf++1966OHDU1NHHLfwtd/4xhtIr14VSZOXB5ufSwfPex7exAmPovJ9lcZHSpWHCPW2fn69wfnW2n2LlW//EV9PA21bCPxlTt/Sk9+43KrMIgJ5mua/8qu/8V/+2N+u1crJMEN05Wp9cWFpdn4+iXtZyrXRsc5mqxyWkVy5Wl1ZaU5MTESh8t4vLy/vmN+1udlSUVkLl8vlTjJEcRUTWNIllJXVjdkdc0k/BpWHpcp6szM3PZXEcWdzozE7Z1ObZgNkMzY+srK8Nj4xoQJF4trd9sRoozcYakWtbn/H7GR7cxAGHEQjn/z3v/LxT/z45Fj9zX8sgLDvt1d/Ne5+XUDw1YAxLNYTCBjVHm7M/ozSY4jqjf/qN1/u7CP48+a+d5btLp3Xe/Sdp9vs1xtXvv2uN3P1DU639DwkjsPU20CQtEOJhACKwBBkBtAggMygUNiDEhFCIAAWYmJiArLAmhEAWIFDNB5AASJ4EQIEBC9wM8AcLKG6yYaCiFD4g50QiiCiR0+MAJ7ICBTYMijAyEqICZC9RxXga3vorgrg1rH3neb1/yFPL8LrLycRUAXzU7v/uTYzt3fHXdfXb7LdbTfeXhPukLNta/ZX++uvWmO9SRm9XYD+s2/39lN4bSfdduyaN34xGTwDb6IEpUNTu3+ZKHzzquH12v3LnN5+fJe8wtvbhtcazu1znTvmiXeV6zt7cduT79ru7Q3dee9d67/Ve2+/643vfeP3fONPeDPfeMexDLuPpv2nYat1hDty2G8veXKh3/7DkfEfefMNvfEnvKXKd730V+mtu/3BfxFb///fxt5aeYuvJgIidu3KP7bppeU1OzutNzc5TtzO+Wh5NavXVa/nZ2eDrU3dm3GuWjfmDv4HQPMXm1f9v15eV2PdKq83wX+Dyq+5RV4db9uu/gVOX++tEG+Nla3pz1+yoddr9y9e5M/VO68Wly/l2VVAXFtPhgOTZDaK9MkzveWlOEulXNXrG2ZmohSnbmFp8N53TwOwcxtpfDqq3P9XpyjeUrkLKIiIWGtXV1etdTervZG9u/PHrb655RdBBNyuP+8UlFsPvO1RxfF2+3vXUwCUrTheec0lgZtPeFM+FYStpddbVXzbXv72B976G95kp2t5tuDTFOErNwaEMjZSQuLpqRICivB9x+onX+rPzkSlKAAEEAKQ9aVnnarJa997m03fPmxufSzATdm/+dqv9WoR0dz8XBiGt1V4QwfdnQbyzJkzv/M7v9PtdJkFifIsR4IiTYVuUh4iEBJ6727drZQqck7Ye7qJXQ4CWmvBIlKeQGSLsYsZiYRZRJRWxgRFFg0AGGOKTBJEFJYtbHQRAGD2W9HlAEUmq9Z6iyuBgT3eDDC3gqiV2qJ7BESWginZiXjvtdaIW1luxRNuPRMAQqU9MwJywaljbRGA770HQKVUQTtVLpVEgIW9K7KSiq+WggudFBEqAAgCIyzeOYAtZgpELEaL91yEyBXFOSciiMAsDxxb/8C7WwBwYzEerQU3lpLGWJBkLh76xmjY7mbHjtbW1vPZyQDgZnoNwMlTk088M5M656wtsOaLfIJXE7kIgyAshlmRmacEANE55wt4ZIEixxFuSmFBEO68A4DR8cYn/tYnDhw8AHfVI9um19uc0BsbG//yf/9X/f7Ae8/gWVjEoyEk8uw1qQJAzFknKICgyBTZFoKolUYB9l4RAaJn79lrrZ31COicA2EABsCb1ImCQEEQaK1NWPJerHPCjgikSI0VESjSZICQSBXLdu89K6UKcCbxgoiO2XnWWoWmwmC9eGu9QinCvNk5AGAWY4KbBCQOt2QFBArGbOWcI0QQUEobo1k4txYK8E1VUJUAauVYwBXSiUqR9wwgbB0Raa0ZxBVZlwwFqDAKOGuJiJERsYgCVgUDNJDWRli2qFAIPTtmf9+x9ne9v/lGOu1u5ZkXJp96ftY5V3BReAARVkQFTYaIYMFwbAyKaKWLoY5FXidAIVvWW+cskVJkgBFREd1c36CvVMo/8RM/sWfv7j/3ZbYHlZ4+fbrT72VZToqccG1fdXR6zEeSZRkzo1ZZnlnrvMJAQqVUkvUBkQEtE7CE2igKtvjmAQVUyqww0ErZNNXaOOcLHLoix5WQHBWZGqnIVvJvoRPDIEQAx+yd01oVtJcFEHSeZQiotQYWYCxGhQaqV0bu23VCC3Xj3kvXXxr6XCmllSIvRArZAylUlOeWuWBy2yJzc4gaDVtgAEXokcgoxZpyBAAnzgEQarGCSMSixDjrlCamQhuhY0WkMDChCgKR3FprrXfOKgZCz4SA4LHg5oUii02U0YpRRESQ8ywHRCAkMj1bLmYAAvLyqf7hg5V+3y2uJHt2lhtjptAnW9b9ZpKugGx0JfeZMUoEQEAxF9SWpBQCFBkxIi6zlpTy4pGxADI2GARKGx1454jEKPLei/IAiAKetzQcCwz6g09/6tM/+09+plKp3K6P7lz+b0//6vf7Ls1DrRGhsXOktrc2t2PXemcFBimRCjRmGQBoJ3Rsx31BEL1y9YV+NrQouXNGaU1gDKWZLSyOc16EHThBCUKwPi/ybhmQgb3zTJqUAhHnHYIopfKbYfa5sdaxMAc1w7rAr2RSQkTWulvACuIYCQEVElmw75oLj+8+9q1Xnm32uxa81rpIBGXZ4kcWERBBKlgjELngn0TxVpFy3gMCEZFyxQ5/YWoRCUBIaWQxpBWpsqkZY6zz5BkRSlTkYwkp471XHIQFfRyIIoNAiEBCW3lXLFprYVaIwL5oAhAJSaFCAaOdkwWFjADjYyrPeWoi6PbcYMgXr3Y7m1l1xOS5yzMshfTudzaIUABxzwMH5mpFYjIRcZEiJaJky84qpRAhz62IFLOZPLFBGGhSRmks6MC1AoEkTUhU0os3V1u+nwMAsy2mPc1m8+zZcw899OCtzLBbwnNrGge3Z0K/elkJgxfOq7XRXfWxCdFZzwbOdgf9UqnUiKIRU3a5G7FYD4KOC4aYKTCooaAKYOsYEZRGBNGkQDGgNkQQzcweiJO02b6kIFQKAJBYDJHzDhk0FBSSJChM4oFIwKASTSLivFNKMYjRAYh473Art8mzeO+VRXAkVy481V19pdVv3VcfE9aINDY2tbmxpqs1sBzn/dznzmWhiogodrlnp4x2Lot0UNFBCj621mhjtE5c7kHKaFJvETFErUQyAS0qVCqqVJt5D7RldCwWQQEhAAqSaAARh1tTFC9JgacoxZgmJIVESIaQAYo0LCEogIkBmbmP6kY2vi9sIWxlxLxyvjc1FY03ovX1tNEIZ6eDky/3ZqbDMCRrJQpx001hY2zn6DgUCasAOgjjOFZISiQIQ2E2pAuwf+98FIVESglmeQYAiFvm2IQhIFqbJ4N+SZc3ljdOfvPFfqvrRaMUUDc4HA7+3MXyXUiaEJFIhVFl12jtPQcOLF9buPL02fVOl0ONIVZV8LP/1d/dWFte6WdtB5txP6cYQQwSsQAIW++BGYDEOAEnHhnIQmBwVqJ6UGmll/fuOVCr1Jx3WsCE5T/9ylfLI6MFt6X3/PTzT+/dt39qdFrApXnv5NmXd87O7ZrbnVvbHw4vXD4/MT6+b+ceEIjT+OrixeP3HjbMM42psqmk2WAw6I2GVQJ1vbUgjDOTB3njRliaDku1/kavR3K939k7u2ew0enkvXKlNLaSDKraUbyjXOqn+blh7+jOA/lS65IejFdrY6kbKunY5MjUjs21jSbnOxojUTfRVdMhcCieLYJXyiulRKCY6SMW8xlWShEjiidSHvwWDoBnArWV9Q0iW+AfJEi4xbfLLw/md4UbCmG96Xu9RGl9fSGLQr1zZxkAKmU6frReLmulwGhkoOvuuEbxNhGg0ZGRQb9vPCsXN8bGvHPep9ZZp7XLgZA0Rc7lztpSGAShYu/jJNZased6OSBFifU0Gmmk6X2N+7Ojj/3J48zOUKRI4W0Cs8383b7evEvijrVWa2g0pldW2r/yy/++URtduLG+vpmEpajdbtZKlc/8xmfqVXNxbdDNsvlD0yOzFWTLioad7kSlbILKjbW1amOURTz4TAqYeWdEDTkJIUpEjt/znkM7jxRTBAC58OLqX/+BH5iZnhWB5ZXrp59++R9+/B+e2H8/gPSS9t/7qb//o9/1Yx98zwcZKMkGP/VzP/m97/m+H/q+j4OAd/nP//N/osSAuGOHHnn46Ps8M1tnAnX26ulL3/yUhdiTb8ztuDZo6zyerDU6g40YNaugVh2N2310bnZ2Zq3TSsVRuZJ523Xpho1rjWp3Y9OmyY763EZvY9NZJ0pH4WavN41Sw8AlDCKe0jwdEAErdFvLPWBEQAQWhQRauZvdoEjx1iKfRAAYC0JyRPSeiRSJUoAogADtXD3T3fHO0RsP3f8av3Wlom4evKoULif7u9AwBrxLiTSCA/FxYolQQKzLsyxDIoVGKW2t1eQBkTQMhh1EKpUibdDazHvf7nOlXI4qoUqt0RoDU23Ugqjms5xZxLlbBCe3l9uN4HZTeMuzobVRCtdW106efK5cNi67eOTI0dX1S512XC/XFPDjz75i8/ihhx+5sbSxsdr7/h/9SJeXr1+/9sDMxA+99z1hOXj85At/+NSzU3v3WSAVBrtnJ1d7Pc+Cgb3QutwLfB/ijJNi0wCJhpR18o7uCgVmPWtNHd93LWkGnUsKKM8zX6Km3VjuLYvHJB1kIJ00Xuuu2NwCQCvulU3oLa22mi7Lc5uDZ0Kzur6oNSJWVuJuM97IvPXp8Pxwc5injv1zK+d1YNhw2yXPbVxBFh2qx9bPuzxnhafXryNi5vN4kH09H4hDpdQzq1dQMAe+sLk8Ux5rgB8R3fHi0DKC8wIIWmsRYS+IqJFYUBwBCCIV4ERbSd4CggpRAaqbGyvALIAFcIsIALKcyWsGZh8aWSk4ee9ucgQX3MEr8QEKXO4hChWn6XA4ZGFFlCZpMoyd8CBJwlJJC2ZxAogAGedJEASkyFk3jGMAKNbLmbOYZ6mzkLlqpapE6cCEWmfei/Xs+fYXuSUzdyqt7ftYn//85z//+T95+OG3rzdX/+RzfzQx3rhy8cLefXvm5nefv3hV2EdGRaG5cunC2x9+Rz9Oh3FyzwMHyiO+EeInvuddv/Zv/81Ks/t3Pv43O/Xosy++EI1NVXdNlfZM5ZLHNs+9smK9SFWVG6YMLEYHAjjIEgeMmlgk8dYqQZYQVGhCQhxkSUkFgWhFSiMPkrgclUIKMnYC2NzcVCUO0DRMrVGZYGZNBMCteLOZbd4kpHEkYADjK0txuxt7Ww/LJWX6bDm3FRX0JK8YVaWgZ9PY2UoQhqR7ScqEZRM4z8iAgQ4cWeGaCkar9YmxmaEdrtu1fr4BClAEi004DQXNO1GgtQEABksFTRMUOAtQ4EIgKATa2gVGVEpjsZXMQmqLUY4QdpTzt40sN4K06NCbCEuAIANXOTM4MKDdCrXSajhIa9WRIAgQUJgrUTmJ4zCK4jTJnGWQ0BitdZblpbBcPMHgFrxonuelUsl7zwq9Z6M1WEtEYRjGLffoHzyeJikwOueMMT/4Nz72vve976572rfkbLtaE5EkSZ9//vnBoLux2ba573Ti06fP3VhcK1fGTGCEVLc/7A2zJ558emZ6qlKpLq4s76qP/7Xv+tCTTz118kqmwgN/8OipX/yFf3BudW0htu1hr78JR0IzaqLHF1Y7qRfmDsqKUjkKayECh2JBLHMx9MmzVmC0CVETYY6ikQxhXsCxIaKgEhTPIKIUSh+BYMFsuLWLURRaa531ECiDGoRIOSIVhuFIqbKTK1CJrvaa75rY121unOfuaK1h1vszjUYM/t7KTLPfOec2Hpo71L28uB7p0ARzqtJz6fW8+2BjZ7fZXXSDPdMz5eEw8m512E04t857ZEJGBMuenGIRZiHMc+u0umn+iIgKVhJQROwBQYCw4CnRqAwVkzPFDEphQRDtRK7nwY32zvlqvrM0aITWYG5FD31tORlry6R3XI0cI3jPeZ4LCwkBQLfdM+O6WBWmcRKWS7m16LRSQb1cdnnG3mutjSYRAc+RNjZJgyCIgtJwGEPu+ukgCiNUWOy0ec+EWwSed7WD28p2wUJEJ67T7Xrrxsemut2uMqFlm1tbkkw85RadUHV0JAirOZkIMU2Tbru/2enM7Ts+s7uZcnT02K44c3GSo1ge8Gi3+teOvm1+avrqE7+9uN7qZ1uKL8sAACAASURBVJlmGNVhfbTxgUfe9+2nv/3A/Q+srq8lWTo9NX32yvmjx4+deeXMrl27RGS9tb53796zZ88dOHCg2Woy+ImJyQtXLh46cvjFl54T6+M8VgGCBacVgs/Jkw7Eey3oTEAAFkQFpj4/3a/ZicBwZp24RLhaG7Hd3kacHJ/bsdzfZPASKIoCl/hePByfnbrRvJ4izJcnreRDKxRUMUz67Jk5onLuGERCkAwYhBkESQGgsBR41YXPgEUAwTOTALubmAlSgFp58IhIwpKjF0TyrMFrJGLvWAo8BUJNoFaG5aVeSZMiJAAVmhIiIjgStGg9CQDkeTaI+6mzgTGJz1vtNhIN0jR3WTawzOK0i3M02jjnC0UZmkBrnWUZKSjg2CTuF+44xy5PhpQmoQ3FMCETail2q99QqrZmWtsyof/oj/7o05/5jFH0w//FD336t39r7969q6trL5w6lee5UoqAjDKVsDI6OdqL083+RqUWjc+M7d0757rr//N/94+aiwvtbuf4Aw9+8vd/t2tMznncqDij3hvNV3T522dOX8qyG4NOlMNEk8RxSQUsBdgPIgCRYmETmCzPjAkcM7OEQWBtoagdIQXGDAeD2kh9vd3yBpNR7mMaoLKKp0ojApgnyUi50s3Th++5P9L6m2dPWmd3HN7bq1O8cKnsHBhTMhESkmMp0H8AwVAklLD11qkw8AicZKBwLhpZy3qDLJssjxGqTtyfjGqHRqYgrDTjbtcNNrN1BsvEpJC2RIqJlKItXCy66ZRjASIFAAyiVAEhuAUgIyAFC6DaQoVBlK3dSwEC0OVSKc9sQZ5CpAmViGhS5bCkVeisj0qlPMtHR0Yl9/VaPU4SpQPnHSDkPnfOa6VCHXrvjTYAVC6VhnEcBBoBEDFN41IUAYLzbmsP0ueFh8P44Nt/+mKv2WFPiGiM+dgPffR973vfnVrqNRprm3VUStUqtcBAGvfuPXroh3/kh5999tnF1dXWxgYz18rVWsn8rY9/7Dve/86f+blfuL6w2d2Qh9/zoDfxwOqf+6Vf/o6HjkdB+Lv/6t/YoLTv+JFkbTGJ81Xix1oLvU5MAnkudQ56mxtgxwEw9W4LplWKSHaLhD5OEBHSvBD5OE2UwszlwMBOPLMIdwdDYaZAOe2khoSByvPvfeihpcUbLUpP7D/8n5589MEdh2bC6tUrN9ajmJh0whXWSlLxgSg2ohxzFOjxoNLPYisSGq2spASRDgVkqP2e2vj40E6XZs9L5+EdRxbOX+CofHRmV37txuje/U0Q78Q5J+QEEEl5XwS9bGUnFtv6XlhACNEBAjMSHp+9/9j8iVpUl1tDnOUW1z0AINyakxXwpahJ+S2UUdravi8mScpobbzbYoE22ghLEATWWqV1lmXabNElsefUpYuthXbczlPrrRijJbekNQhHSnNmo1KJmb3zRMgeNZkojJRTBRcQIsltfuq7WsBXBWvbqlApVSmF3vnnT75y48rlf/Yv/g/nMk1SKZVs7hSw0qUb168+/lhntFx9/zvek4Mc2HXg/MLLoS6fb7eWvvYSi2eiBw/vPX9mcWXhcvnEDhmtSC9PekmaZdqEZR9+8O3fe/KJZ8Qzkgb0GjGMIiAdx+kWxK8HIjIkxRAXEfAAAEigEJkRERkg0Hp2cvpCb7U0HmFKaeYG3dS5jPNkrFT91te/vLs2urG+0jMyNT0Dud2Xl1S1fDXvPTJ9MF1vv+Cbc7WJsZV+fWxszQ3vq8y32+2z3DoyPje4utys1hcH/bGpXRubLevtyqBVm2ncaK8vdDYP7NnlxJR0HaSvGR2gBxbnEIGAFKIScZ4LI3grOIOAwiD8G2/70R2N3WeXTi23lxQVebYILDed0wACpLam+cVEWAGqLSNIwkhERgeIyOyAoVqpABB49Oyr5Yp4HwRBHMdIZK0tlUpFdlCaZrONubcdeMf5hXMX1i6y8CAZarBaGwAB0Ox9P8mEOAyDxOY5O2TMYm9sUFIlVxUpoNGcu6WG3kiwtlVi5v4gsdYtrzaRaWRs9MWXXnjggfvOnjufJlksSN4/fvLMn361+a53v29+144vf/mr3Y0NsbyxtvGht72j7G1ckdFyvTTgZGf9et2t16JW7kkHURSJ0d6xsXDplXNKKRYAERSYmGgw+9TaHdNT6+tNZy0DolJSBNvcBMNFxJuQuFsjBoUO77rn4kvr569cLSvzf13540Bpo/WNpeXRqZl6udaYmOTFKxgGFtiL1CammnFTWCEpqpQwhY1Bb2b39OrmpvPOBiqolu26H6RpY25mqb0+sMnp9asxOya50F4CAMt+Kel4MpP1cjePU+9T5y2xoNcaRcQzkUJxAixKEYIW8FtLDtDv3vedU7XZX3/0/2zHm2oLvBgBQDwbbbYw1dkrpQwZBFV4nwBAI6EggkYkTTrUIZFyLmfGWq1WjSqc5kma1uujSEEQhOvrq5VKiZmr1VqS+jAM4zhZ2FidHZ99YM+9iR2mPvHeaKQwjNIkIUXW5kppIVRKZVlmCqByEWGX29TmqXXe5vau+1ivK1i3NJb3PsnyLM0cS71aavfaO3fvarZa5XIlHqZKkTHiRTqJffGVs+udXmKzG8sLpUapUq9vNjendswvtBd0jqPR6NC6ZDB0FQTBxGbDJHHeM+BYeeyBE/c/9pVHi8GtQY3WR9obrbIxJa0ao/X1jZYiAkJ/E6H6ZkgCGGO897eIgbxwpVaZqFfyaBSEzAgIYoQqz1Ot8PzVqzeWFmLw3kFEus/JM82LoWGl9dOrZ0NGDLnv89PdFWYAkTPrN2KXJ4Sn1peMMalzu2qjuykaMF/Ouiemd/XWWksq3jUy0fCknXQ9hhSQJ2L0ikBQWAS9AG8hMqJWSiEQAgoDohzddeIb5x7txC1CBGAWtQU5LAo9khCKL+JgcmaBrY17BQqVKoCfQZCJMiciDkEUqHiYsBMETGxu+x1NChHjLHbimTn34gUTm2VZFmdJP+1Nj0zWo5Gr128QQikkGMowjot1hNLaAQpzGIVsXRRFKBSKJOmw3+8xEyLeLlhv4Ni5S6V42FdEczNTm63V3PvpyYmVtdXcOTJKAOJh4oXTNLly5XKSJeVS2QE1RucnJ3c9e+qpqxtr5Urp6vXm2tqGc06AJh4+LDUTA+fGawKbZysry48vD7ZyMJkZMc0d6iBJhlonmcurI/UgDBBw0I+RKB4OFcj87Mxg2EcQrXV/MExSa60Vb7/xxDeaccvUw1x8VgC7o4hR1zaXj8/tS/vDvD+gWjX3mYAYjaMmioKAiMjQiFKIWKLKgb3Hz149fXzXPcubS6fXbszV62NaXem02HNQqpgs1QyjKgx0ac1moQlDYUAWcd6lwk7Ai4AXRkRN5HJbIGMDAUOR8YMgQkobNJuDDjACFED2UiCte0ZSBgEJNXtPpEfKIwLST3oAXoQFVbGBSoRaBdY5DWh0RKhKQSjWs0hAphqW2eWVSkUxGB3lWVYPS4NBXA6iejU0OrDepcmwHJRGoor3vmSMUirAMPfWizCKSwZKqQBVgmlmY0QSKDcak3nCirQIFHRAcDf382tcOneaQmtTVlQfqV6/lu/fs4NBr6ysxvGwWq0SIDAb1LVyLXO59x4Ih2mWo52aGhnf3Ziol8phtNkaVCvVY/c8EPcGzXK2nG46mw3zmDyGJpyZbuyrzL9y6rQIM4IItNqdSqkEZDLHve5gbGykGpYAgPM8CMJ02Dc6yLI0DEJmrlQqWe6QTL/fD7T+3g9/9xee+fpK2olAMwopxc5ioJQ2lwYtrTWMlssmZGaruKrVrsaePfOHlaIrN87mfuC9tewW1lcVsF5brwwTAD48taN76uL4VP3qYKPpk9RZZPnGykUlKvY+bq3NlccntDZRyaWYs+QigqARhL1iKAixCAhYpNhtA0ZEBr5pExxphcwhKYW6oDXx3tdL9eM7jx+evWd6ZNqY4NzCmc89/1kkBaCIVEFLLuLTNN2KNxSrFHjvvbVbBsf5PHUIKYEqQKy11kEU5t6CAy/ovUvzPAqioruVIDEQYEChE1+qlCtRDUREhIXRAyIGpSAMQusswx3Yvncc3zrYbgqZuYDjP3BgL6eDR+499MrlRZtneZ73ej1kmBitvett915bbZ48dbq1sYFG2TXYNT+x1F1xmGQS1Am14wPTe3/mx/6brNf/11/+D1dX1xV4BVKg4bssp8rW4oIBQbA/jAdxAsxIpHWw2RtsdPpBGHjnTOAE6CapPTjL3e7Aec/CRMQAX3n0ayu9VV+i2Zm5QRKHUTAcDtrD4WitNkxT5kxpPUjj+SCgPPNOl8OxWjCR5RkAZX6Y5qKZ53ZNPr127UVoTjSm/OLCC9cuTE7X2/1mw5T21MZbw17bJgfqk+TlbK85HZXnyyWwDA4MaPSoULE4ZCFAoq1YRCpCNrFAlkUkAmYQIAGjjGdBIkRd1AGRhw+9492H3lsOSkkWL20ubfSbS+0FRAQBIu2ceMcAws4CUBEE65xzjjVSYEyWps65KAyJlLUeBLQW731/0HcCeZ5b68plDsMAFCV5ttnrGqPjfJimqTB7yJEIh4qUFhHnHAGGYRhGofI+z3P27HxGRMYEb2AK7+KEvuXoYVHiXWSCfbt37Nk5tbS+2miMmSAAIGPMkf1z/+1P/e3/+NnPXV9cWNts29y984HjAVggsiLWMbEqBeW4M1i+sTAYtJc7LYsIgozkIEeR3NvV5gqC3EJ1BwQBD+gFsFKtdDptUmpmZtJbKyAwWsuzvFKuJEmSJ+nY2FiaZiy4vLoigPcfPg6LFEXmo9/zsXQY/9LvfXI97c80xtiBRlUdGUmytBRFaWqBCFktrFzc7KyUIhPnvdS5zPkj87s3T7/QKJUWB8PxUT4G5YFR1warjGo2qo6mVKHRNrjdo7Nr56/Ww2iyWnerreqUGSSDxEvm0ZJTHotYK++UdYxomFHpCAo4ZEEU0qRvxjOgQgWCWgUIGJnwB97+0f2T+zeHm988/fXLK5est6AQgQgiBBJPWgdbgEjixXvwDIhKK/CY2VwARKP34gHKYZlI5XkqAmEYgaBiHB8ZHw4GkQlKYakWlYShFARhGAYqSHXC3vbjAZEiIDAGBBBy7xgxYK+cQCYZiMMC91deYwpvic02s3h3vkLr3OLiYmt58ctf+FxQqYTaJIBZZgNvlpc2f/7n/0VUKe+YmQtMVK7Xq6Xynz32hd0HdgXjIbHE3g7SZNDNT730ci/t5uLIsRL0LAzkLEyNTh2d3vv4+rdgC6FYANAzCmoEzK0IGGaKhym7zARBlmVZmpkg9CBDl7t+x+YWUFkUQlBBcGFlyafpxuJvTk9M9tnGWigwjcpIznZ5s8WhzjIeI1CANW32zeydndxrjLq6ek56S1qysqlM7j5+T22MRuoVTZccdrpriFGgoZUMUpV10zRh99jShbCm+oPkQmt578SUJSYTqJxQAFmEvIAIUOaK7SZQQMyF7fAi7JkcIIOgoAZEIQABx4GO/ua7fnRmZPbZy89968zjnvy+6X33zN8zXZ9caC1+7fRXQdCxR2sL8jAGJwLsyRgj3gszClpw7IUZ4ySzRpDIO6dZ2Huf5UQ6Zg/AeZb30v5YdX2sPtaNe77nrXNRKRB2lnPxoLXGGJ1zBfC9YKKdclRGkjzLlSp0lXoDU3ir6Nsl7pazWgQWFhY6zY1m32bNpUceefcrZ8+5fOjBYli/vN4Gbr3j3e/J+eIgSdYXl08cOXL28oXj9QN1VNozAFxZXvjDr/2Z1Z4PjTIIIhiGstIjpdKV65dWzl7RoIhIxNfKFSQMgzDLMgAxCpnAO5sNYyAS8c6BB+oN4jzPENE7LyLO5gDinH3syW/lTk4cOPL++x+eGB178g8XVJrFg8GATJqm06PjK+tro3NjGtAynzl37frZhUA9h1oCYx944LAHnQ55emr+4KGD/UF24dr5p9YvD1ymtTaAISpSSgl6EE0kAImSqlJWRIkoR4ZIAeaegYjQyE16PxDx7ITZmEAr7QpCpYIPgAEZSCEioeBHH/nY9OjM10999dnLz8yP7fzuB75nYmTKeduP+3EWO+cKDx0Fhq333jF7o7VAwSvIgQ6V1iColCnCYb3zRMy+IEvAxlij0+kQAJEKVJhlqUJlyIRUtmhNKN5a59kJElJuvZBHRCRCZBbOHesgr1TLxhjvxVprjLlDE73GG/2qE3rb5jv7HBEsy0a3jUSbnf6p8+estw4ZFafeWYG15sYx8T2fObYnz50zZXXk+L0XLl88PnKEJMu8m5mePnLk4NqwfYmH5EVQlzH64APv/OyjX5msNh46cOjUS6eUECKUS+VhPIyicGysnud5mqSlcpRl2eTExGAwzG1aKQVjYc05J+VwMByEQVAqVXq92KZWK/2+D3zwm6efXly88awlQJRhDgL95Y20v2LZqVJU3jWaDXuZqzqxWe6cd4pEKdVqb6y1E01w3wH98smr9See8N5P7tlRjfWRWq0X+JWsP12vjw+FyxMvp82Hp/ddPnU6Gq3snpzPr62O7xhbsR1xnPvMg4BnA+C8R0TvXRGtUPBtMItnMMaIKEBEpQGUQg0Abzv49v3TB5+7/MxzF587sfv+Dz/43SDw3IVnztw400t7iKoa1Qu9bChgbYGZFQdGC/tiYl4JKyLgnFdKUCkENNoAoAdXCSPnHFhfC0tEynvH3oVGa63CIKjUylmWWZcaE7KYOE2L7ketJyrJ7vryaNQuqVhA5Vxap0ZngVfXVeHh3iZPWylAt82p4E4nNADkGWgTLS02O/HQOstGrayuVioVUuSZu90uEQXaXLl0WZwnNFmWtQe9E2QAyTkmtMJwfP+Jn/7xf/SFx7908aWvgPWh1VUq7QvHotj1ebjSXPcsAqyVavf6WqtOt99sbQCgNmYwiAVkYWVVRExg8mHqu4Moith5YBLE9V6rWOJ7Z7/y2FfbMtSR8RUdlUv1frm92snaXcU6UEoGuR7mmsF650n+x7/3s+8+/pAulX/1t3/zK09/LWdrHVnUQ05XV5amx8ZrqaWBNHZN+rxP+WBj2CuNjDe73SHkr7QXqjtnlztN6HZP7N1nmaIw7GaDIladFSR5LiABEwKKFwTRCtEzIhmlCEAK+iURRnQilbDy7nveu9pd+cqLX5kf3/nhB757kAz/6Jk/Xmwuaq20CgghTywJhkFIBM56hQRANhd2iKgDY2zOznFgAgTxjgnBCSOSIpOluQhk2YCMKbiibJ4hYWptPx7eWFkkRC8pInr2aNB7X9Hpu+YXdta7sJXkV8xUkrEd7YM/IucvVL72xFRuX+PJeT3HzqvbDVvRj8yZi1VQqo+Op3l5MBiMN8bj4dBaG4ahiBTe6HKptLywWCmXTRgCOCQxijTggbHpQa+zCnl30FtdWbp65UrqcnZeRFLnzi9fTzUY5mGSACEo8gAus+T8llEXoCwbG6mOjIwaU+xDU6fbb/cGw05PGBCJKHfeMTgAMEp/8APf+fUXH/c2f2D3vsbExMbKSjgzu7zSLymDSBlw6m0uHgHQ8/r6Kg9T6+XQzFj0/vsZCIQb1R3TY7sPHz7y1FNPLrTWr2cbg02fSq4UZc6fay3n4AFkcbARcDcHBtu/0A5GKiM5O9yirPPeMhYcKhSAsEJEz7qg2CpcNL4gDBMAJPQg7t7dD5fC6EvPf5EEPvLQ93nvP/vt/7TRaYZK7509cP+e+8cqI3EaX1699MqNs85lAABISilh1ga1VsLWewzCUKtiM0JMGERBSSll8xwFvffVSkVEiqlqqRyy+EAZAiqZABGVDou/PcnT+fHkkdlzod6KeIXXhDEIERw9PJyfu/GHX9x1uwBts4DbBev2n4xWiuRtD95r831vO77rtz79+dOvXMzT1KU5kTq4d/eHPvjw8y+e++oTT7e4NTE3MzE7mTYXnWUBWY67YTUYNtvi5YWXXlhYX/bKe8KEZSj5F08/E1bK89WZIzsOPPPkM+xRIRIBsAAUaQOeQKYaE6VylNtso7WhlK6WImFutXMmFAAvHgAVBgyOEJPecJjYy82lp3/310NtJhoNY7Rl9mlSqZSFWNVDQSHPTuzVhesvBM+PT0z1B4N+d0gKHVBVxxevnLl67VKvN9BBFBgThgoz7VFmqqNT3sQC17PuiZk9yfrmqiRTlbGRVEKAprWRCjWq1LEoUIQARFqDALNnRtnaSmBh1MoUC2BFxKIA4J6dxwbJ4MLqxRN77p+oTXzrlW/2+91Ahe+/9/0P7H/wFjvLnpk9x3Yd+/LzX3LekSAgehCjtSLlnAMUTSY0ATtnUBttQq2Y2SgFIIRks0Qbk6VDRLTWIaJzVmulFbHAIInDIMzyvB7Fj8y9HKpbue93KYJYr/sf/N4bKdnbqSHvqrr0NkEjojAqhYGy1tYrXK2qkaqZnZ7a2Oyyl6gS3X/voQ+9/97NzZW912c7vfjAnt0zu+aa6wuAggqSPGYVisaXL1+5uLicB+wPT4pnQUi8LUVmoja+udB6enFza3MHidkj4s3sd0HEJI4Fubm+XqlUjTGjY/UgMs12E1EDKGbxnpXWAJBl+alTZ5yLd+6cyoaNAzM7q1HlpVNnktxznA2Gg7AWVrCBCKxRHJy6dnHl2mJ9bGKxtXJ58SqK0YQ//Xff/rZ7DwO4MCg5y5Xn8ijf3KzwuXjDOleuN/IkVTEEJvQYos0UBSVDClVJR9Yn3lsWxx51UUQJF45C451oTVqjc8wsCgtSJGDra+X69OjMhaXzzrkTu08MbXLyymkPdN+ee+/f9+AwT05efKGQrchED+5/4DtOvP+xlx575Pi7Or32wsbiffvue+7C88d3HquVKy9cfrFeHTswu886d/Li8wcmDjRq4xeXzwvDwR0H1zvrSZ7snNwFgNblJy88b23umbvJEACHLjHiUPyHd5yLVP5afIkiRQhvpuBvXanVeLL2BMp3btFQ3ea63b7zfse6UcqV8tPf/jZ5/8rZq8tr3YKYynuHLCdfvvDCqZPjjYmgXIk8snW7ZmZGq1WDEKKMl8o6UB3vD87tmZmYudxeXi6wARTm4jlHdt252cbe0blTz53SWvPNqZ8AgDAgMLMJgk67Q4qMMb1eb2RsNE4y4a3AXQBQSnnviSCM9Pu/851f/PajzbQ3psJ37zseRZVXTr0STFb7kIpQMFFxBkwQxN4i4JWlhUvdVJXLURB6Ag1Yq45iaH7rP37q/e98R5a5q1evv+uRh1YXXjJ+oFR/KR028xvWeVH4retnNUMqbtC2m1GtwcoigWgQzV6BQhESpswxIZog8taTIkBVJN0XdIBFNh8AjNfHIxO2eq16NDI1OrW2sU4sZVV6+8GHFNKFGxcW1heds7nN33PiPUqpvdN7ng+rU/XJ+bFZAD48f3iltXx0z9Fh0gfvD+84uH92P4NcW7rs2O2f399Le4Rq/9yB5fZKuVTdt+Nglg37cV+RKmJyQmUAADAUgR219kxl0Gzl4xN6Y8OPj2vvYTCwo3Wz2c3rNaVJ5c4bs0XUmPWfzLNLQXToTpv46s779iUhs8+9zThOHIo7/cq1NE2PHD0eX7zoRBxgBrTRytY7y0ePHVteWdlcb/e7rVKkEBUAWW/Z+yRLH7rnbX/n45/45Bc+s7b4vBcfMBomRIhKwWAwWBosK1KeRbjg50RCEiQRFHGbmxt79+1z7Lq9ngq0Y99qthQSIOXOURFQhyDgbG4/9/k/jQMfjlQhx87Qcrer69VGY35i5wwoDAJFmc26yUYgkxOT/8tP/0+z5YmgHL109oUXzj8zEUZRZWQ46PU6rbc/eN8TTz69tL7QzY7d8G73ziOtG7hzempleXEj6aXip2ujhvPVXnemMjIZVTSpxOgsT6y1pI3SSDfD1RUp79gDGK2ceOcRUYmwBhIREEKgclDy3vXintJakV7trKR2OFZt1Mp1Frl/74nd07u/8PTnJ0YnDu04hIgIanJ0aqG5dHzvsdnxeRHeM7W7ZMKLCxcQ1fzkXKffHquNHdx1z+Xli47tzqmdCpUVu9Jc3jm1WwEmNm/32l48g4AAiyCA5JkodXBiXRDOXRwcdJVLV4fVsqqP6DTnK1cH5y4m+/dGUaQuX00/9v1TTz/XnhwP9+0tD9tfM7MHbzM128t2U3hzCi/ec2/YzfO81WqRDpxzpEhAkiS21i6vrAdhKCwmxDgbetFz8wfGJqaYrFZWM4l3rbXVtZUlYEAkz957r4Vclk2GY0GOgA7AQxE7ViSqEnnvhVSz2y+1NqemJqJypI1eXFoaJCmAIkKttlKZAUBYTBh88MMfeub0c52sX43CZLAxOTlTLuseM1RVKdANMJ/4yEcm6rV/9/if9dntnJ55cN8JClTcW+90rutub1djpG3zhYXr/+x//UVBxWEA6N1gkCSDdreXxMlRM1olWoTkvun9V59/qdIYGSmP5qsb5elqnA2cRhMEwBaLmHbvbwYykAIOtub2SIQBGSVIgAHpigmamyu/+cVfy9lb73/ji7/hva9GtciEWAQ7IL506Xlx/uFDb3feKlQCoEmvbCzfv//E7NjsanttbnIekZbay5Ojk0ZHy73VKCxPNaZeuPjCQnN55+Q8Al5ZvTpIBwXUj3M2tXnsrEfw3qdpprWyAuDtdKUPACzgRVbX03o9yFmWF5Pp2SgM6d5j9TMXhjt3BABg9FaIYZacgpsdt014CqW1BRBdlOJaqVQqyKXZgTgIVNjtdt1NXI3hcGiMIaRupwMgpJQTtuI325tXr1w/sP+YsBLEc1fPvXL53CBLcgAP4EGEMLGZKJzfMT8/t4MLfJibDAaIWOTWM5BgsLDSOn/pxvJqe2G52drsA2kB5fwWFznfZHln5marudnrWoIhuN9/6sv/+vOfWkt6Ogw9ASo1WR+ba9TmJsYmTEnQtJqr4vJ+t720tGDJV+bGB+Q9aWR9pmETUQAAIABJREFU+vTlleW2zWg8jMYyh8OhCU1YrmYz42ftRtsPH79+ZnOy3Bx2X1m/NhirbLIPgrIRrcnkvMV4qdSW5gYgRvAoDgQItTFKqy2ab1Kkw1ygn6dhEIUUouD/096bxkiWZedh55x771tij9zXqqy9t+p9eoacmeaQnEUztEAalm1JJmwThCyYsmzYsAEb0A/BgGkaNiDZlkxDkGVKoAValKUZUpwZLrN1T3dP711VXfualVm5Z2RERsTb7r3n+MeLzM6u7mmNLPuHDL4fVbG89/LFe+eee+53vvOdUJlqUCGgwhVKqe3e9m53J1TqrWuvvn/7gialCYs8GfS7zrpKFG/sPqiEFetz7+yjxx8xpI/PLpogaFfb8+Oz250NowOlaH1rpaLD2ISKsF6pzU7MNqKqQlRKKSKltYAyWofGAsCx+cr8TPTsM83xtp6bMs2WiQy2m1opmhqLyo6szz7dOrFUBQBntz+KMxxaEXw0CY2IRKC1Of/k4++8/U6WZSKYZQMRiaIIRCrVapqmURwhURCGhVjLohAmJ8batdbO5hpKAURXl+9f/99+009EeHJSA2pSCijQ4dZe76XNN8cLo5URUuyFDtqPIigWISp788pwmJakK6IQiRhYfMlIAqRRZ3nni8s3rmfi2Xkg+MVf+MXV7c1rK7fAOibOnR1q+Nvf+IM4iO4Fzgbx5srKy9t/cOzE8UGyvZ11wEXtxrQRXJia/cpf/OWN7a2bd24ntjCzY1v9vTRPZ2uNbmfdaLC24DhMuRhAbj3288IAe3aVoEZoRCS3FggVoGdfPjfwkBUFEikSa5kKIkEvkhbFIEu01qE2e72ORqpWKoq0tXmWp1eXLz939lOD4eD0wiNGKQQ4tXBGQLa7O8tbDxj5zRvvVKP47tZyYKJ+0nfsBvnw+uq1Oys36tXWwuRiGJiN3bVry1eAoJv2TBj2Bt1bD24BQGoTBK+QiZAUKAUeMs9cWsjS8UhATi5W5RgI4MREcPt29uwzlSCgxcXw2GL44ayNgyNqR/AR3OFh2gwAFIWPY//CZx7ffXDl1nInK5KkyF06wD4BYDWpWmuNUUEQ7HY6EzPtWjV23q4+uNvZ7D5y/pEwrjD4+ZlJEpVE0gFE8gEXj7fGv/rCz/yDb/3eycfOBQN/89pt8F6RKkU4RARGdQTk2ZWSToeCHOy5HOwHE7ePYxNFYb/nfvZzL/6z17+TiePEL5ixVtPsd7ZdTLvpviO5vrsWhmFbB1G1JoX9o1d/qDuDL3z2p6WFbR1X9v14PbBAw3Q4SJIsy00Q7Lvi+tbqmu0XAA1bWUr0GWq/o3Y+NXX6zjvv91qVqq5U9/baU81UIAhMKTuCSkkpVTUSmWP2PiAToAJF3olWKtZ1o0yk4kpQFRJNuhZWlVBIRgSd90qZi7cvzY7Pn1kcxcUISIiZzV67/FpZHHDt/hVAFM9vD98SligIL9x4T5hRZD/p3165WR745tXXSyzKeb832L+xdrsMSLQ2qfVlLJ2lXkEEIrnTkXEI0OvznbuDE8cqQLy8nJ87G0dB8FBZDgAggNLjh72BPpo0FJGPKf9ClF6vf39545lPfy7xrzNt9QfDQb9f9iFyhVVEZdnFwtTUl7/ypZ39Tcf4yquv/dznP3f33u2tztbe3uDLL37lv/iP/vPf+dY//vtvfZMFPMtgOIgo2Nrp3Fh7eQLjVlQvZTTKigOEg/oAKEtZSqcqBzYnzB9cPQt0+wkOUkD5/e/9cW6cKLCB+sbr37lzb3nh+LRyerxWy4ocA+URh8NkutUswKdjan56Zj3b2N8Y7Et6euJYV+zkWLs1PrY/6Mf1alvGZ6utYxiNVfQ6p5ikaXN8e7DXRL28s1Y9Mx3ubHqfT5w8ZXOCwisFLssIRAFqpQtnA6MR0TNb4FAREAlzoEygjDaIBGSQtHaeiYwXIERGFPbaKESMo/BH7798dvGRE7On4rjinN3ubl5fue58PtZouoPO7cyslIqiSI7Im6VpqrUiKpOwHAYhszjvkzxFwEo1TrJEWECcUiVdB7xPtI6208qi2QeQW7f7zz7Vevu9vSz3zbpRqC7f7O9sZWFERtP+vqtU9ZOP1eNIB/E5+Liw/UO0maPwAwAIMCndqI+fPXOaqLq9vd3rdvf29gaDIQAaoxEgrlTmFxZOnTr9yGOPfP+VbzvmrODvv/LK51/8qemJpla6gtDbWr+xfD1TQjYQ0ht553/6J7+dabHe7Qx6m2sbkcdQmTKhhqSAJQhDZz2SYmalUCnwzhkTeMdlfYH3YALjvFdaWV9EtaA9No7CRgf1er0ShF2732zXGLiT7jMLsSJlAhMYpRSjQ3fixAm3vHpiafHt7XtryZYOalNkfvkv/GoQYRiFa2trg9QuLTxaCM+l2/dXV24UK4tjU7XVjCJY7u48WZ3c6mzf2txsBHVALeid9zTSDHTIHChiZhAOtA6DAFjYoxUfRYrBOe8mWmPMIgwKsRJXNCnvPTEYrTc6Gza3qR2+e/3tS7cvKqXyvAACYwJmJsDxxpjWRitNirTSzN4e1Cszc2YyQDxopCssZUbRt2ttAAnjyNrcM7drbREW9CBAJID+1nZjsd4DxNMnK+9d7CWJRLE+d6Z++VrfeWmPBfWavvB+f3E2nJ4Ou/suilW18YVRl/MfU1HxQRK6tC0RQaIoCuI4rkTRiaWlMAhqlXhmatI5l2UpkY4r1bhSHR8fn5+fV4rKciQhlVj+zg9eQQAS+dV/81f+4f/xu5vdDRV4Qw4ZLErUatRq0WBtO1Hpft473pw4Pru439v33s3Nzr37znunT52/fOX9EyeO7+7usudTS2fefuutRx97/O695XarpRRtbW+cOP7opfcvP/eZF/7Zd7996vy52OVhtSIE5xaPzVWapyamX712wSrfbjbAemOCPPeGdAzaAHWL/EHaWzo+uTnsLdYnJ60fY/jmd//gqTNP/pmvfuXSxUuvvfbar/3l/3Bts31l9WYWBJOLC93dB1luzz36yM2tB4bC6vjEhDHK22pgeoM0SS2SIfCWbSkFIswgQohe2HofKBXqkJ0PjBGvEeiL578oAKWOoyqZgCLlVLG8tfzOjTf7+/txJS6hnygKAhN69oS0OHn8uUeeK8uWqFTEOvJYscxfCACOWoELlgpfhwgmHLQ5l9WdB4SmDGGF4dZO69n5rWZUEClS0GxgsxFeurZ/7lSt2/WkMA7x7MlKox7Uq4REYXw2qj07AhM+4rY+no9VhjJlLYAJgrGxsSzLLl94Ryl8/PHHbl273kuKU5853WyNtVqtWq1mjMFSxJKFFTJqQmB2X//uH6X7SRrmwfkFERLmRqNiSImDk8dmOXe9Zg1yd6e7JoIsrP0gmm+6ho6mm1ILYtVy3qWRb5+c2Xb7ZrIWjDX2Orv1uQlqV9rHpu/urVUXJnbtMJYmiddMd6/cKmpjuhJGJq7Ugt6gWyBzmgRBnFtf46Lvcu/d/Z0HMlbrZ2lLV4+Pz1ikelTdTwbXrl2+cvXSveXbb775WlpkbPLb3QcamJXv2cGl7TuiEBHe374Xq2is0qrHzV4/L6xXyrDNyajDW0dExhgoCsmtilQUBE6cywuNZQwiAEAEiCgHmr8IAohL08cnmmPXlq+sbq6meRKU6Tyk6Ynp03NnZ8ZmP6jpG7Ej4TC1IlAS6+UwF1QC54cxNQAwMBGxZ2OUd4UgOOeNkdzDD+7O/sIj92s19eQTTWEgguMQwxFNm1a7ZMugs7gz/NpUySf78dvHAKTW2uPHjkHgX3nn1e5+F7WXmsdK9Y07l1iSuB69fvXNEydONIcTKMUgGQ6yrFFthsStdjMrCieOiadbk26cx8aaK3HR6+0Mc5fkuSHWQVgNg7nJuf2JyUu3bjTqNSfSS5N16Z94dOnRE2c6vp8gPHr68fubm/suf/JTz37vvbfjppGJeGvoJhvVLhZjS3MGsdqqBto4dmNRNTTh9trWveX79UY9rAZKUwixs0mj3c7zolaphlpXHfnQ/PTS2fTC9U999rl3Vpbf2ryz2J5nksXF41ubm1OTU2fPnFvdWJtfWKiEZmLXDAepTQFssXByQUSsS1NKFQg75x2I9dVataKDzJIXVqS9uJKOp8kIeqN1FIRxHA/tIAxDcUhIhDhS9DyQeBwZBYCI1KL6c2dfePr0s71BL88zpXSr1QpV9NEy0Q88An5onV/uU1bLPRTk4EgvgoTFIyCSVoF4BFHL3dpLt+e+cPIBEiAd5nA+7IoACwt/8J25p56rHvJkHsJBDz/5eK2jqenptY01aKjlveV6WyeN/FhTPXl6vJdEN7a3Vm53J+abc2Ozve7OnQfXK1F8/ulHJ+fHdZGHqPfybo7q/NSTn/uZry0dW/j1v/ff5lF0st2+t7rx1spqEFWfO/nEX/mLf+m3v/6P765u/I//1X+D3v13f+dvdIr+XG3sRGPiJSt9mzx76kx/a6s5NvXc4qnvX7+AlQhrkWpW98FmaefExNy/98LPnDuxmO8P94c95xx7X/9srLWuViphWNWt9o3t3V//7d8c7g9MEOzt9+pjdRJhKa6u3T752MKfXHvXG3quOlUDfXN34+U3vv+ZF55TEnqt7txfu3Hn/vPPn9+5M6jG8Wx9zGkf2er0zEwv213rriqlEMQzK0R21qaZsDekkcEXTgURSFmyRiYwQJhmGbNY5w1CqexX6hjJSJsIS5lgPGA1IQKiGW9OyEjcET9qNyJHs3ijD8tPysdcir+VngKO+BYpYy8Wz4zIgIIKBVgJXt5sDgv9MyfX61F+MM3SyG4BEWRzm/7R71eHWfzUc5/gqkbbw5XQAFCmzdFB1k+RuR6qz586+fyxxffevPrY2NSfefLRv3bnd6vNhhef5gMyXhnsbHZt6BcnxtPc7klReGg0miHDYL+bFsnZ+el//6tffPPC++//ww0htbK28nf/97+zM+xHWn3zm99QAXXSnmnUuulgo7s7tGkBvNnrNtoTzharezuCREjeOSCsVKuRMs16FX2S9Du7e/w3/ubfRsJKFC8tHvvyV768P0grVTdpDCcDTaTiKLe20ajHcdS3A8O+k/a31/skUgGNtXpewOTM7IUrV1/87OcajVq/366eXLh78xYne2NxND89F+uIGZZOnv7h668158YrYcMWiVZEAhpJo0JmJYAi4FkfzIbWFqUqg3POs9OkFVFpTDLi+gPACJwYyWUfKYAhwtJh7PS2nbUgYrRpNcaU0gBQ2GKnu1WJqs1qq6zGZ+aN3XWt9GR7CkDWd9YRcWpsGgRKkcHbG3fyIhtvjk21pkvJVcc5IeUuz2wqIkYRoNzdix+8c/zczODceHe8mgfKA0DuVCazb7ymXn9X0iyv1z/kmeDDcEM5MkZww0PsBgD03lfCihUIAzVTiV989PG//tf+5qvv39Ta/MZ/+atPz89m+5maJkAtYMI4qta5Um8sb289dfrRO3c2KqZWF+lfv3o1390Y7FZU6xvffe3myqpVBOgubdx+MOyoMBiq/LXVS6CojwUOuqlSr9y5WG9XQ/bfuvBqo95M0uTGcLNZq5pAB1o3TTAR12Jtit7QgOT9wgT1vOBf+7VfazbqgSYC1ETOyrDTGfR6WWZja8mTTwoDZtw056PCVPX1dPfp2RPp9t7b96/Otxdnl+bev3l7mOb9wZbAbpLuz8zoPN9Ihiu+qHkHC3PHXJKemF+8cOfG7NmJAgQBtAJFGoGMCrQtnHVAZEhrUpZZWCRQDhEFQmWIISDFjAfzy0igAaDMSY80sg4e2AdP5PLdS7vdHWA0ga5Vmp89/1mjgyQf/vDSy2cWzj5z5lkBEBHr3Y8uvzLRnJwamxaWN6++EQTmyy98FRCB/euXX7354JZWSmv15U99rVltIpLWBoSVKsUEkYKwKHIWYaGLG62Lm22DNtCelAYyJyeeuHb3WpKssQj+mGj9IfN6uEoHAIrCJknSwOrd1dsB+Ha8qJjeu7/lg/kkT+486M6ON9/cXrbQV0qsy0Bw/cHKubo+Pze/dfHyxFQ14fz+YHWwv3l1Y6UH6Rvbye2tzs5er0+g0uFji6f7m3uTk+PpcB8jssImNuPVukGyRRFTXJJuE1eAUUDUgCCgQCMszcxOVBrTjfGXXvrh3pleK251uruBCf/P3/lHyXDw5//8nzu9dIKdc+CJ1bVbtybj1hOnHv/hO2/VFpooZL2M1dsZJATBVL3dGbpesT9WFHVbPLZ0KgzM2nq/P9iv1WLkdLC/sZP0b770cl3PRuHlp559WpQUPmdxCCDCZahUxuIlwRwBEKkMVJVSjsU7r5G0USjMzhvSdDCgEQGREelgxgARIUI5WLjhgY6DUvT5535ua2/jxurV3d7uzPisHOSGjgbPhOjLBSlRGIXAUmq0Fd7eX18ea7R//rkvJdmwWW8Ji2Nrba7VKOQCBGczYUYACyDeISErn1sxADHpwhVCXJbc4UeivYfefnyMVabhsiw3gQG03TTZs94H5j/4d/+N3/qdb03OnvjS1776P//dv2Vm5hOXaNSFh2PzZ3/qhS+SEAE+/+gX9iB55d0/utW95bJ8Ix/u1UyeFRF6Mp6tj018cvrYy+/deezRR29t3BlkgwwkCHQn6RmtnXdACgR84UQQgQAxDqJAIYm4NBFrTp6Y/5GYPM89c5IkS0tLv/IrvzIc9OPYjLS1gZDo/ZvX2mPT89UpBNnv9+doIVX85sbtKCIf6D+5eYHZB5X4scnWVr+np8y71967fWe51+/3k57YAmwRa5107aljk+3AXL57a3N77eRjC0ky0OUABdFagyITGLLoSsiAqBwVWmtfFCRgtCYi661WqmyAcRAw4Yj8NDIsAoCDqp7DR1V+joXNnbOl4mS5ld00jgTOwp4/eLoiZW63XCSGQdTpdl668P1T86fbzXFARFRECgnZCgKMJFPlAJRGRMGy6ggFvHNaqVa7vb668VC0d+iPPspjeBhuEJHC5lpTmiZO/Fav9+0fvbHQCn/xK5//0pc+Q0b/3h99bxhU6pEej6uK9MClFVV/+0fvLB1fSGzK6OvNyYjiRHKvRRDEiohZ28/SYTrMs7rPr/r3TRUvvvf2zOzY7rDXisLC543xdn+YNOr1tfVtpaNmc/xAsE+5IhfnAFVoQo20vvagGgfNZs15v7axeXf1wa//9/9Dq9X4C//2n6tFsSItkgvxyZOnL965t93ZUVEoiCrQvudOTUwrybY5X2xOasab3c13Vm4v1Vt9HlZblWeeX+r20yRJ2PpskNhhck/Wrq1fq3Yq4+PNubmJ1kzdcVEqrAFA7qywKix7AS9KQCEDAniWwntGEADPTDjSG3XsuAyuR+pleHjrWRgPiHM4WiiCAFprsyz7wdvfR4JWdWysPl6O/KIo7EH1c7mlzlat9cIgkOS5Z1/qzitSn33m8+/devf++vKd9bvPD7vPnH5eALyILSwzK6W9eGEWECQkAUJiYfAEIoKQGe+F2/WmIsXeg3zgmY6azUMe6mG4AQCI0HnnvLOghaJOL/2tP/zRP/2Tt02k03w4N784ee7s3v6OAVOBkDyRMt768frkUiW8ef92TBQAOdEAYiQn5orSEGgtcVWH/87nv7ago8sXX69VGsdOn3n51uULW7czsNlel9AMk34c1x3LbmcPQcr4l4gorhkkFuigXb5+cWZmZqI14YfF8dnpX/63/vUwDJCoWa0IA4uwJ+X5p5569qWb77989Q0X61oU7vf7IVCzY1tzrcFg++mFU6vvXp2q1mbHp5sb/Wi8mUoxG1UCEh7TRmsCECuPPXWy0+k565vN2th4E1DQE4swgjDHJhAdkNIqCFAcsyBCqAPLzjErhoiMRsXWhYEJ9Kg3DAB8kPQsYy0RQjoakxzCVIExjWr9ydPPBDpsN9pGByyMAM45Zs/CZbGZQqpGcdkuJXPF/mC/ElUAQXhkpD/7zM+vH1v79pt/2O13QcSzZ/Yi3qJkRQIlH4NIKcVinRcAUbp0n4IY5kXR7fXYs/eHqkw/mT7W0a1MQsVxjCAB0ed++rNTY5X+fi9N07BeiyLcTPZ3eahSDCnoS+rJ3rt3a1oP5qZab7558cUv/dmtzf6w6GUuW+vs8dJUzCS5DUBXGtU7N27vQ9DppnudJClk0N/L88KHogEBuF6vOqZef8AspMkBizB4Ic5jzeILDOp9Sueremtv496Vq/tpEZgYADTp5RuxZyZDgvbETFNmTkeNKkcBsM/y3ASB2HSnwuu99R757928WG+HO0myu3lvOmru7Gz+3CPnd965phbbDwbDalTRqIg8BdieaZd9LPreCnhrLSE6VHHA1nsFnp0v2AuK98zs2QuKQiklr5QwFK4ArUMdIEMZ+gCU2DsjEBGURKBDgGAEn4sAorW2cG6yNRWaQABYypZg4py7cf/63ZU7JxZOPvfIp4jUVGv6we7Kt3/0rcIWodFnF84SEihMsuS7b/5JGITOukAF8xOLhGBUGKjAedQoQOi9N8FIh6wM4JhBWFAhImkBZh5mKTOXHS5+nJf6kGE99F5EiiIf9pN+MtRKfemF57Xht95+ezLSs5Pjuyv37vWT+NhsxLotpl5pbsEeEcaVqFGt1yrNImWlYidumCXW5mG9UqCytmhU42E/u7/+YHl4Z291Y9jZY+HxyfH508eDVoWEBRnIZ26YZIUKg7AaAQoQMDMwI9iCLSB3817UrGqkP371SmdzCwBssbXd2RUQhaC0rtZqwv5is/FTX120lnNlJ5qtIAxDHSitht6KRmJxPuuRC1AQcIi5QPHynfefOjG30t/tS6pFhTpKC0sCzbiSO+ikKQtrwqKwjUqNAR0LI0VRGFdi6fUEnTZhScQgIkOIoLyzQRCSCrxzjhmEHPNBZgVHbfdkhD8dQPDAUoZPQoBT7elqVJNRbmbkLJQ2J2ZPE4IXX6s0SonDp888E4TR8vpdhXTu2COPHDvH3jOzQnXu2KN31+9qE75w7vzpudMMMkx6hU0AxBbes0ck58uFiAAgESKxoBJGFhGN1mVBJWBmY8zRKPCfY1gfAmcRkXCwvz8hrdmpdrsaXXr/4i89+9SzJ45fvnjh9M++eKfb+c3vfG/sxDGqVgcKCkajK83GREdq6/c6z37mM5Wqnp5rNB3sp4OtNE21bRljk0wHqjlezw1WKwu+mPTi4moFQ0QsDJEXTwiIUo8IwRMyAzCPUERCQKTy/mqExGfLWddF6PK0kCFNVqwrtFJa60TcVHsm1cF333ktiDVoLFyepWmv1UiLdMFXGnF4lzufmn1k6879TiOerNeDzT60Zm8NdyugpoaYR+GT40ud6/fqk82h4qks7GXJuk8fmzmW3V3rteOZuFUbFCG6XjrkOFZKoQgRKKJRQxHvtDYoDF689VFcy1xW5ExCN1dvTDz+02WOjYiE+TBRe+RfABgRIJ84cf4QoEDEMqfYqDRefObFkcsoOTpAYRA9e+a5p089fbA3AJRNgdRTZ55+8vTTZaGKFy8CN1dvsSfnC8dlmKXYywisL0N59AIjGqbzXsDV61WlS4bGh+L0h3DQD/hYDwH/iBjHlfmFOaU8sM2LQZX4p86f+0/+s79+c2WvXa/8vf/lvz5daxaotpNdE0UArEgbHUT1Vlr4qF5j8eVvwPKHadrrp465nxTOewEvRtBohaZAkqIAT0KICsX7spOORsVl2HEQixzI+AkKVmJ9e3/dtjwyMseEVWEJSm+LgETbkoTKMhFnVKnXuoN+pdlY7m0Gsa7NtPNs6HJO2Tdnpx501rpIjx5bvN/ZElAuDsPJMdfZ7Cbp9MkTl3cfDMEtTo3n3SLp2Qy5uTizsrveBD8zOeELlJJnWy7laLSgK6MQUSCMIsgsRZExO6WQAd64+ubs+Ozi1HE8WCEekBnpsL/f4dyBB2qRAgd46gdwVzmhHtRgjQ4sy1IE5PAgARHhMp5DRCCh2w9u3Vm/a3TA4hWyUopFWA78JIyI3yB4wIwHRNLGNOqNo1LIRwfDQ7YFhzHWUY9FhA9WV6YaYxgTCEw06oPu/nqHcjW9MnSbu4PpuLkn2lR1HMYd6mptpqZng0BNTI4HgY6jhrPkgZADFK9Q9+Ni4DIXIThkDrUicU6MscyAyCCACgDKek4hKHXMDCoSAsKyZxWzlH0nc+88AoYRAjjvRRhZDJKIRyREzQADEUZwIVPeN4pS16twRTh8eeVaGKoooHc3bxAp8c7m/tXO3bwogOG9tbuOwEtxaXe5ooOhLUT4Rxu3mAQJ3l+/E2hFpO53N3vD4WR10mrtSl4DESkQBC+syBAiixBCGATC3nsfBqHWyhYApL7xyjefPfX0mcXT7UYbD/J6dIhLyYGNlDcDqUwkl93LEMCPDBGPBmQAwMylF/Tsy2mzZGWJCLMoRcLSHexdunXhxuq1YZGRIi9loOHKAqIyccPCDASAKCBEjAjC1ruQgrwovPcmDA6jpo/OgIcffoC8H1pYnmdcrUVhDKagMLjV2akvzn3hZ59/6eXXX/jc8wtnFm598+sLU08AeQQKq/qlV7/76ec/k+eZIcXsb965ffHta4XNkjzJbN6cnCz2dpE9eV+KhNfj6ueffW7twYPCOY+UFMWDjdX22FhqrdI6ydO8KCpxTIikVOHcfp7G9aqIKCKDSgQqYZmUxWQwBCeFdzlCpRopBbboG1BRFDnh/XSoFBltAlJRHVvtsK1bLsQBZY+Pz9MwX5ZeNarO6tomDXfy9OzYDGbFNd6ZqbZmVGUj3Vsv+o83Jmzu7sn+TLVZFbWdDkibuTCOEVe21hxP5TYnpcpkPzMrAAXovRf23hciIkCI6JmFJSsGSql3b7/x6sXv16q1wAREKk1T730URXEcgUcCRURFUSBA2X62sGWfnrLvpheWwBhEtNYqNZpzGBiRoigurPPO5XkeRIGzNs3TwtsgCAoTxMWWAAAP50lEQVRbIIFzlhkIAxEQQGAHgiUGRoTeM6vSZaEGLjNCIlK4Ik2TLM+YucrVj9rTUTTrg6nw6HciQkTWWe99oGDf5Uls/td/+rt/6Vd/4a/85V9MrPzW7/yT2vQcGOXYeVfMLk1Gqv6HP/i9555+cjgcrKysPvPUc4+dP7uxuZG5PB0MN+6v+ySr16sCgCzC7onF+a+eelafeGZ3mFIYbe7sfuP+73/t2Z8P4rCz1/2/vvH1rLf/8//an222moD80is/3L66/MKLL87PzyvSt+/eef3tN1oTLRJRijjPa5VK4K1z/Mz555954nwy7IfVOAiC115/9eqD+15YEIeFz0LTfuTswsKJvsmF/cmxuY2de1UVTtValX07G9ez3C61JnfurFQhXGrN4NreRL2eAs9W29v93YpXp8bnt6/cmRivBbV6oxDx0Gy2gkqstfaFFxKt0DpfWBsYQ0oJiHMOEUmr1GZKKYPaOeu9c1KwcFKkwzQp6xqsd3ZYDLOhwlHPWCi7IRMBkvOslSoFmI0xAELZaIpUipz3pA568gz2vGdjjPMuYbSFRcLcZ8lwWPIBHTvnPZQefdRxeGQNZQtIEk9IKEAoKEIAgErEt1vN0jyOGsxDtnX0xcNJaEQExvZYm0gVJNoXjfH2le3kP/3N3xoLKoktZh95dCjD/aRjFEQq1hQ3G+NFeH9jb7AwfryYb251sqg+vrdyL/UpaWieOD5fqSTpNqBXnhDd/NJcpEIdKM36+Kkz49P7Fy5eeeKxpwXEHXNXr1y//2D1K1/6cqvZAoHOVm9jdfurX/za8eNLSvC1V1+9e/d2c7o9gERIIolMaEIQZFxaOv7UmWcCQ5XAMLAUWR+6qfP76cALQmz2q/nrw3voMYrMH9+5pDSmRb69s1YLTL+fiofvLl8KNG3n2WsPrteCqL+fFex+uLPM4iy7N1ZvRO2gk+zGLpkLa61KG03gxCmjHFsCYmGlySM7YE2okcq+1GEYpimHJkDBIAi89xoUIMRh7B0LQxSGY804GSZKaROYPM+992EQaK2LoiiKohLESitEyvK8JPp57xlYlZOm954liirOu0EyYGJBBgSbW+dcEATEJaDFRZFbZ0WY0ZX+yosXQRFmsWUlLZEAeyAUD0orEAYgy7bT3yn7/34y3HBoSPqheF5EsjR33oFgW9XHSY1VgntTW42lc0umnRjz7tbd8SCMsbpc7E3E9YWgCpRW2liPEyUd3QDb67Uidfx4e7OAU62pWDVa4ycjsIO9DlXDK8sX4naUKHn3zXc6g2xtfzg+MwmN2jvXrgJzrVaLG63K/vDqjdtbW9u1Wl2HlVZ7srM3WF5+1bH3KCfOPPq1r/3c6sYNUUbXmp3OigLpJvszixOd/t4gGdy9eyeMgmjCPPHk01Ozx6/cuDLZHNsjvzvcng7jPTdMOKuGYaRM5gpNOFlp6KH0uKgEoQZUea4VBcZAtq8VhRpBNAgrwiqZPguyA7IWitX9DZ0EucvLxIcWA0gKQIMQs3O+DG6QlGcpCosApaSH9YUwiIJABSLS7/eH6RAQyKOGwDrH3luwypGICMkg72MxWhuDgyAISqw8jCvCiIiaFBEa1PVqTYRNEABCwT7Pc6UUChpEFmZkANBaeyiXQ8iIIkBEwqZcHop4RkYW0oYBgRE9ClCt0T6I5D7GjI4CC+Xbh9kNiEiK1tfXW8cbRabGp6bWNtYrWKtQbdjJdN2M2+q51nxnb8t4qWo92Oi36uPiVBvr6dbmVp6emDtmVzemmu1umk3TmB9SZSLK97rd1c364nRAZm+484Pk+34aYJLWiisrt2Slc+1+54aXkoVN0IRvv/b73osiDSD1ufDbr/6e8Ch3eOrk2TgMbWcwe/zY1mBwpjHR3+2EYby8dW3Z33TeS8CpGD1U1Wo7gOpnzn/q/R++Gk42lpoTjb3s2MTszd7685NLq5dvmFZtrNXkB93p6Zk7w71nxpaWb9zmanxq+vje5bvzi3Ob+eCJysz22lZHm7OTx9au3F2YaoumiUQixzXRrA0haqWEmEWUIm1UmaEDJUDoRMA7UCSKxPk8y4wxYRQN+sMkSSEGQkqKRDHpQIkTnydKBUqrpEhEPIhoMuBLolXZbxiyNCVEAUjTDHAkFy+ubJzOpIlT4TL5gkiKvC9w1BVRmFlAlX0SmUul/dH6kYgAQYi8eK2UF2ZArXVJdtVaiUgURQ9NeR+dGUdT4UOurISAtQpTZ7e31mxQAHORJJ2s4Lix31vPXfHOyjWjTABBb5A3asG1exfR2rvdVUA38P0LD/amK429/YGQf5Ds1oNWRbLOcFifnW22W2Z7hQi0JgPomTUVUUiff/E8AWrSI2pAuYz3XKbVlFblAtqzgNLWggnN6SeeWdm8Pz493e9uqHYNM440kFGEWgkikCCHWi/OzL9/68L8U+eGznUH6/WlmU6vE5toL+tPnFkcdncG+XDhzML2fk8bnaCfObPU3VpJi+zEU4/e313XpLEWV2fH9vY7vXx46vy5WxsPjKiJhTnrIV9dJqfzwoEKBFzhRaNoJACxzI59aLQwO2e11mWpkWWPCqFIkRgQrM9NYNAIC6MEWitVarsjEgXOeaNNYIwweO+MMVpp9uycDXUgIo7ZsSitidBSISJhFBLhYDBgkVpU0UolWSKslNbOWSLUWgsICzCPMoNSkjWAVRkXeiYs8+GskFAEBZizWjU0Rhd5Jgdy3J+8fQzyjkRpWqwur+u5ytv37tphDix5XiBqo4xYFu+FRRMhiM0zRvDsBbT40gnTxbJmRcRoo0SNta7v7e1676Mo7g/2Ef0BRbtcbhNAKdtZch9REJgZAYgAEAlJuJRzRgLUOnzqKXvx6ruZS2vV1n5voywuJtHMrI0uwSGlyChz8uTOhevvVaI4DCu9/c3vj4SZ9Rt4yzOX6v5EN733qPhVvIyIwFwutQhYWN6iq0RQloaX1TIg9Ja5EleqglQ1CoHVKItM7L33I8xTHDM5Khf9eW7CEACQyFuLWlCJ84XP2VkmUt75xGZRFCIAe8fMRFqRYc+JTZVSImKdVVRiq+gsa609e62Vd7mQMsoYY6wtnDAJBTpQATnvnHgGhaIAoRKFImJdYZ0tATClxLpyyanKqUzjqGkVACohYmTNAG6QDCYnJ9bX1g7pqEfhz6MeSz6WNoOInoUlz1fTmnjVImIBBE1Ka+PtCPVnD4gGRTx7UCQk7IoygCAiECFAgwTsreTbnRyFBCB3KWj2LIQ46ugI6AWU0o49lQkcJIYD4BmFCNlzmewgEhIpOP/Rez8ssw27xR5iiEyWWcCJCFqGEuxxiOz2Lr3riPPBgHtDgJIuQpllpVMRVqy9pzLVSlaLMCkSB4heRBAP1s/AgTFEgFSwcBiZjCi3NjCqGgPknotcKahUIpZSXxxqtVqRJsJsjGk1GoN+H0SiOGq1WsPhsMhtGITNVnOwPyRQYRhqE/T6/SiMyqbkaZrEcWxMMBgMyhFVq9XYuzRJtNFBGFnvrXPGKKONiIiwUiDiiiKzroiiEImKPM/zXBsjRGXqetTUQzgwgQg45zzbkokDZZ1FGWaBx3IsCSKABtSZrG2spUkiIyrox0yFR00IPjYJTUSKAmTpP+jqHlAg3hZpkonAcJAX1vmR7xQpFXqIBIA9K1LMfKjHVWreCzCAKKQSE/Gl40EUL0hY9q4lIu+dNhoREYlZtNJKk2c/YoWPQgFGICQidBrIiwDKQYETAqrSvbmy/RkhIisoBVCVMIAgECMhYQnFSmBM2ci0NOIyF8aeAbFsIYkARMqDi8LIe8e67PmBKoC4FpvY7CZbAqKNJmI3SInQCwJAkve8eAGmXPWyjngPADpXnWxLBMAxDGB3EJRMd1LKe0daq0whlq1TYDf1Qaht4VhEedWXAAGd98iABWltRLj0KOW6sHCBiHj0KsACWBjIKFX2eWdSANZaKw4NGiKPPgqCNLMEZdMyh0SKyCjj2QMgM8dRhF4IyXbz61dWi+5aqONqpUr0MarJP3YqPPRpExMTqmT8oyCHRc+KcOGC4dDawqZZbm3ZhwGF+Uh1sohAqa1CiCPRFRAEHJFZpax2Ejwgixw1wQ+Q4oNzEWIJZOPooQMRsQCPRowIcIk8lsAbsz+guH0A05UnJEThMpYApMPkEMABzVwO2ASjIXhQ0yCHdwZHUQUDk1JYVtcSgCIkj4SKCAmYvdK6HGsi4kmUVixMAmV2a5RwLqmnIOW10RFQEREQlYgoVapqiQCQIjm8Swyk1KiiUAARkEZVFUQsAiY0tWZ9MBwWRSHCpV6iEYUgpBQQl/AHKVWv17q9XoaslIrCMMIw1CYKgkolQgCtDUEsjpHF9zgfKEPoHYsuCdmfRJgZGdZDAOmnP/3pU6dO3bt3z3uPzOiBMHT5HniLYhUQgyqrS7hcqiIeriuAAQQYRB+I2EBZ5wE4cm0iwkyEgARMBzf1wPb9B/lWwNIrUhleCAB4IAASwpJBBxpEyt2UUswEpRUisoyi/tHtH/VrLpMnZcs/8CBEChyMWHciasRfQ4U04hwTg5SCoaU9gJYRJwFAqOyviKOeOSQEoEe6OiMjRisMAIwozEji2SMdmOyoE3Tp9EeJHEQULgABpCiDUGZWSAfZuxJlAig7PAKM6i9KExUlIiBFh4YHGW5hcQeUUCw5hIgIMFSAcWXYbrX3tna01pUKZdWgGmkfaK5ElTgWIvGORGlSxBwrFGEBKHVoxyfG4Z/Hx3q42TgAbGxs/MZv/MatW7e6vT1jjGco8sI7V1hrbemxoLy7B1yPD7EhGQ6/GBXOYdkd++CgkjciR65MDma78jMZkTcOwkMYZVlLosPoDn+QIDtwt+Xzkod/8MHgPsysgQD4w0d5EIkeckFKIxThw8yuPzinwoOyh9K4SsGJkR3JyBWVRVkHQ2T0svyPDy9nlFAhIDiApfGARF/SAD8IimFUgepLa4KDmrHRjxlxBUE8jjwYlXeDR3x6INIiggIG6MDZMBFOTk52BwPvfRiGgdbVOK5EsdZBEASESGQUonNOKRIRUjoMo1qttri4+Ff/4796VO39oRB+5H0fgrzKp9Xtdr/+9a9fv349z/OHUPyjbz95+2Rk9nDC+oR9jn7y0ZM/dIaHzvyTXMwn7I8HNZ8f/eqjF/+hCx6xDQ7/3MGoOnrgyB4ADzlWP8Ff+YRPDn/O4ZmPXMsopQ1HBv/HHv4wveIjewZB8Pjjj//SL/1So9H4sRd9uP/h7Xto9Vj+pU8O0P50+/jtoXv2Ew3DfwU2LIs4Dl6XLz6aEvyxHuv/3Uv5BNP85G//dPtXevuxNNOj09//47N/st18ss19woH/Mpf0ydu/6Jn/Za7z/3/fHt0+kAB96N+HTnT41UN7PnTIT7LbT/L2g8X/v/i3/9/93aO35Wi486ffHr49vF3/NwLY4uZXxbRUAAAAAElFTkSuQmCC" }, "Event": "nodeQueriesComplete", "TimeStamp": 1598022340, "NodeManufacturerName": "ID Lock AS", "NodeProductName": "ID-150 Z-Wave Module", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Entry Control", "NodeGeneric": 64, "NodeSpecificString": "Secure Keypad Door Lock", "NodeSpecific": 3, "NodeManufacturerID": "0x0373", "NodeProductType": "0x0003", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Door Lock Keypad", "NodeDeviceType": 768, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 46, 48, 50 ], "Neighbors": [ 1, 46, 48, 50 ]} +OpenZWave/1/node/49/instance/1/,{ "Instance": 1, "TimeStamp": 1598022021} +OpenZWave/1/node/49/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 4, "TimeStamp": 1598022021} +OpenZWave/1/node/49/instance/1/commandclass/113/value/73464969749610519/,{ "Label": "User Code", "Value": "asdfgh", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 261, "Node": 49, "Genre": "User", "Help": "User Code that was used", "ValueIDKey": 73464969749610519, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1598022021} \ No newline at end of file From f757e258a7697969a3f415675ef4dbbff69b5b6b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 23 Aug 2020 18:40:51 +0200 Subject: [PATCH 302/862] Upgrade pre-commit to 2.7.0 (#39180) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 25e3db656da..c5b01a3898c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ codecov==2.1.0 coverage==5.2.1 mock-open==1.4.0 mypy==0.780 -pre-commit==2.6.0 +pre-commit==2.7.0 pylint==2.4.4 astroid==2.3.3 pylint-strict-informational==0.1 From 7874711936d30ca2002d5b2a161ce6d22f92c988 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Aug 2020 18:41:11 +0200 Subject: [PATCH 303/862] Upgrade discogs_client to 2.3.0 (#39164) * Upgrade discogs_client to 2.3.0 * Fix pylint issue --- .../components/discogs/manifest.json | 2 +- homeassistant/components/discogs/sensor.py | 25 +++++++++++-------- requirements_all.txt | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index 53dc30d6b39..2d8e308a42b 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -2,6 +2,6 @@ "domain": "discogs", "name": "Discogs", "documentation": "https://www.home-assistant.io/integrations/discogs", - "requirements": ["discogs_client==2.2.2"], + "requirements": ["discogs_client==2.3.0"], "codeowners": ["@thibmaek"] } diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 40f27135be1..4d78b540a0c 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -122,22 +122,22 @@ class DiscogsSensor(Entity): @property def device_state_attributes(self): - """Return the state attributes of the sensor.""" + """Return the device state attributes of the sensor.""" if self._state is None or self._attrs is None: return None - if self._type != SENSOR_RANDOM_RECORD_TYPE: + if self._type == SENSOR_RANDOM_RECORD_TYPE and self._state is not None: return { + "cat_no": self._attrs["labels"][0]["catno"], + "cover_image": self._attrs["cover_image"], + "format": f"{self._attrs['formats'][0]['name']} ({self._attrs['formats'][0]['descriptions'][0]})", + "label": self._attrs["labels"][0]["name"], + "released": self._attrs["year"], ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_IDENTITY: self._discogs_data["user"], } return { - "cat_no": self._attrs["labels"][0]["catno"], - "cover_image": self._attrs["cover_image"], - "format": f"{self._attrs['formats'][0]['name']} ({self._attrs['formats'][0]['descriptions'][0]})", - "label": self._attrs["labels"][0]["name"], - "released": self._attrs["year"], ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_IDENTITY: self._discogs_data["user"], } @@ -146,11 +146,14 @@ class DiscogsSensor(Entity): """Get a random record suggestion from the user's collection.""" # Index 0 in the folders is the 'All' folder collection = self._discogs_data["folders"][0] - random_index = random.randrange(collection.count) - random_record = collection.releases[random_index].release + if collection.count > 0: + random_index = random.randrange(collection.count) + random_record = collection.releases[random_index].release - self._attrs = random_record.data - return f"{random_record.data['artists'][0]['name']} - {random_record.data['title']}" + self._attrs = random_record.data + return f"{random_record.data['artists'][0]['name']} - {random_record.data['title']}" + + return None def update(self): """Set state to the amount of records in user's collection.""" diff --git a/requirements_all.txt b/requirements_all.txt index 34bac40ade0..8fdc47ccc8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -485,7 +485,7 @@ devolo-home-control-api==0.13.0 directv==0.3.0 # homeassistant.components.discogs -discogs_client==2.2.2 +discogs_client==2.3.0 # homeassistant.components.discord discord.py==1.4.1 From c86c522eb194053e0b1e38cdd0923c6c3bdc7a70 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 23 Aug 2020 18:44:11 +0200 Subject: [PATCH 304/862] Upgrade py-cpuinfo to 7.0.0 (#39155) --- .../components/cpuspeed/manifest.json | 2 +- homeassistant/components/cpuspeed/sensor.py | 26 +++++++++---------- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/cpuspeed/manifest.json b/homeassistant/components/cpuspeed/manifest.json index 3cd4be6f9d3..ced8344ee55 100644 --- a/homeassistant/components/cpuspeed/manifest.json +++ b/homeassistant/components/cpuspeed/manifest.json @@ -2,6 +2,6 @@ "domain": "cpuspeed", "name": "CPU Speed", "documentation": "https://www.home-assistant.io/integrations/cpuspeed", - "requirements": ["py-cpuinfo==5.0.0"], + "requirements": ["py-cpuinfo==7.0.0"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 34e9c5fee25..4f352900012 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -11,12 +11,12 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTR_BRAND = "Brand" -ATTR_HZ = "GHz Advertised" +ATTR_BRAND = "brand" +ATTR_HZ = "ghz_advertised" ATTR_ARCH = "arch" -HZ_ACTUAL_RAW = "hz_actual_raw" -HZ_ADVERTISED_RAW = "hz_advertised_raw" +HZ_ACTUAL = "hz_actual" +HZ_ADVERTISED = "hz_advertised" DEFAULT_NAME = "CPU speed" @@ -30,7 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the CPU speed sensor.""" name = config[CONF_NAME] - add_entities([CpuSpeedSensor(name)], True) @@ -38,7 +37,7 @@ class CpuSpeedSensor(Entity): """Representation of a CPU sensor.""" def __init__(self, name): - """Initialize the sensor.""" + """Initialize the CPU sensor.""" self._name = name self._state = None self.info = None @@ -62,10 +61,12 @@ class CpuSpeedSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" if self.info is not None: - attrs = {ATTR_ARCH: self.info["arch"], ATTR_BRAND: self.info["brand"]} - - if HZ_ADVERTISED_RAW in self.info: - attrs[ATTR_HZ] = round(self.info[HZ_ADVERTISED_RAW][0] / 10 ** 9, 2) + attrs = { + ATTR_ARCH: self.info["arch_string_raw"], + ATTR_BRAND: self.info["brand_raw"], + } + if HZ_ADVERTISED in self.info: + attrs[ATTR_HZ] = round(self.info[HZ_ADVERTISED][0] / 10 ** 9, 2) return attrs @property @@ -75,9 +76,8 @@ class CpuSpeedSensor(Entity): def update(self): """Get the latest data and updates the state.""" - self.info = cpuinfo.get_cpu_info() - if HZ_ACTUAL_RAW in self.info: - self._state = round(float(self.info[HZ_ACTUAL_RAW][0]) / 10 ** 9, 2) + if HZ_ACTUAL in self.info: + self._state = round(float(self.info[HZ_ACTUAL][0]) / 10 ** 9, 2) else: self._state = None diff --git a/requirements_all.txt b/requirements_all.txt index 8fdc47ccc8a..10f37a99d6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ py-august==0.25.0 py-canary==0.5.0 # homeassistant.components.cpuspeed -py-cpuinfo==5.0.0 +py-cpuinfo==7.0.0 # homeassistant.components.melissa py-melissa-climate==2.0.0 From 04c1c1a279c75e410d1bae34c40ebeabb6c66f52 Mon Sep 17 00:00:00 2001 From: Paul Madden Date: Sun, 23 Aug 2020 10:46:11 -0600 Subject: [PATCH 305/862] Support Rainbow radar site in BOM camera (#39129) --- homeassistant/components/bom/camera.py | 1 + homeassistant/components/bom/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bom/camera.py b/homeassistant/components/bom/camera.py index 3bbd9e39164..b99b12916c7 100644 --- a/homeassistant/components/bom/camera.py +++ b/homeassistant/components/bom/camera.py @@ -54,6 +54,7 @@ LOCATIONS = [ "NWTasmania", "Perth", "PortHedland", + "Rainbow", "SellicksHill", "SouthDoodlakine", "Sydney", diff --git a/homeassistant/components/bom/manifest.json b/homeassistant/components/bom/manifest.json index 854b42f68d3..a712c8fd080 100644 --- a/homeassistant/components/bom/manifest.json +++ b/homeassistant/components/bom/manifest.json @@ -2,6 +2,6 @@ "domain": "bom", "name": "Australian Bureau of Meteorology (BOM)", "documentation": "https://www.home-assistant.io/integrations/bom", - "requirements": ["bomradarloop==0.1.4"], + "requirements": ["bomradarloop==0.1.5"], "codeowners": ["@maddenp"] } diff --git a/requirements_all.txt b/requirements_all.txt index 10f37a99d6d..45c10e0df93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -365,7 +365,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.bom -bomradarloop==0.1.4 +bomradarloop==0.1.5 # homeassistant.components.bond bond-api==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1e9c2c64bd..748e263f6d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -190,7 +190,7 @@ blebox_uniapi==1.3.2 blinkpy==0.16.3 # homeassistant.components.bom -bomradarloop==0.1.4 +bomradarloop==0.1.5 # homeassistant.components.bond bond-api==0.1.8 From 9e3f7ac8df38e10a38ece8d6617c94a916925c07 Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 23 Aug 2020 18:50:12 +0200 Subject: [PATCH 306/862] Update bug report template for new Logs location (#39183) Logs were moved from Dev Tools > Info to Configuration > Info. This was already changed in ISSUE_TEMPLATE.md but not here. --- .github/ISSUE_TEMPLATE/BUG_REPORT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md index e60aa00a448..bdadc5678ff 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -20,7 +20,7 @@ about: Report an issue with Home Assistant Core - Home Assistant Core release with the issue: From 15c101e85d05ec0b669c7e7d7d4755d16445917a Mon Sep 17 00:00:00 2001 From: On Freund Date: Sun, 23 Aug 2020 20:34:30 +0300 Subject: [PATCH 307/862] Add pin code support to the Risco integration (#39177) * Pin code support for Risco * Remove unused parameter * Fix imports * Fix typo * Apply suggestions from code review Co-authored-by: Chris Talkington Co-authored-by: Chris Talkington --- .../components/risco/alarm_control_panel.py | 51 +++++++++-- homeassistant/components/risco/config_flow.py | 38 +++++--- homeassistant/components/risco/const.py | 3 + homeassistant/components/risco/strings.json | 6 +- .../risco/test_alarm_control_panel.py | 86 ++++++++++++++++--- tests/components/risco/test_config_flow.py | 6 +- 6 files changed, 155 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index b0786c77c79..2484772d5f7 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -1,12 +1,16 @@ """Support for Risco alarms.""" import logging -from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity +from homeassistant.components.alarm_control_panel import ( + FORMAT_NUMBER, + AlarmControlPanelEntity, +) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, ) from homeassistant.const import ( + CONF_PIN, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMING, @@ -14,7 +18,12 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) -from .const import DATA_COORDINATOR, DOMAIN +from .const import ( + CONF_CODE_ARM_REQUIRED, + CONF_CODE_DISARM_REQUIRED, + DATA_COORDINATOR, + DOMAIN, +) from .entity import RiscoEntity _LOGGER = logging.getLogger(__name__) @@ -30,8 +39,11 @@ SUPPORTED_STATES = [ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Risco alarm control panel.""" coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + code = config_entry.data[CONF_PIN] + code_arm_req = config_entry.options.get(CONF_CODE_ARM_REQUIRED, False) + code_disarm_req = config_entry.options.get(CONF_CODE_DISARM_REQUIRED, False) entities = [ - RiscoAlarm(coordinator, partition_id) + RiscoAlarm(coordinator, partition_id, code, code_arm_req, code_disarm_req) for partition_id in coordinator.data.partitions ] @@ -41,11 +53,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): """Representation of a Risco partition.""" - def __init__(self, coordinator, partition_id): + def __init__( + self, coordinator, partition_id, code, code_arm_required, code_disarm_required + ): """Init the partition.""" super().__init__(coordinator) self._partition_id = partition_id self._partition = self._coordinator.data.partitions[self._partition_id] + self._code = code + self._code_arm_required = code_arm_required + self._code_disarm_required = code_disarm_required def _get_data_from_coordinator(self): self._partition = self._coordinator.data.partitions[self._partition_id] @@ -93,21 +110,39 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): @property def code_arm_required(self): """Whether the code is required for arm actions.""" - return False + return self._code_arm_required + + @property + def code_format(self): + """Return one or more digits/characters.""" + return FORMAT_NUMBER + + def _validate_code(self, code, state): + """Validate given code.""" + check = code == self._code + if not check: + _LOGGER.warning("Wrong code entered for %s", state) + return check async def async_alarm_disarm(self, code=None): """Send disarm command.""" + if self._code_disarm_required and not self._validate_code(code, "disarming"): + return await self._call_alarm_method("disarm") async def async_alarm_arm_home(self, code=None): """Send arm home command.""" + if self._code_arm_required and not self._validate_code(code, "arming home"): + return await self._call_alarm_method("partial_arm") async def async_alarm_arm_away(self, code=None): """Send arm away command.""" + if self._code_arm_required and not self._validate_code(code, "arming away"): + return await self._call_alarm_method("arm") - async def _call_alarm_method(self, method, code=None): - alarm = await getattr(self._risco, method)(self._partition_id) - self._partition = alarm.partitions[self._partition_id] + async def _call_alarm_method(self, method): + alarm_obj = await getattr(self._risco, method)(self._partition_id) + self._partition = alarm_obj.partitions[self._partition_id] self.async_write_ha_state() diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index af2df0ca577..03fbc322075 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -13,7 +13,12 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN # pylint:disable=unused-import +from .const import ( + CONF_CODE_ARM_REQUIRED, + CONF_CODE_DISARM_REQUIRED, + DEFAULT_SCAN_INTERVAL, +) +from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -79,17 +84,28 @@ class RiscoOptionsFlowHandler(config_entries.OptionsFlow): """Initialize.""" self.config_entry = config_entry + def _options_schema(self): + scan_interval = self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + code_arm_required = self.config_entry.options.get(CONF_CODE_ARM_REQUIRED, False) + code_disarm_required = self.config_entry.options.get( + CONF_CODE_DISARM_REQUIRED, False + ) + + return vol.Schema( + { + vol.Required(CONF_SCAN_INTERVAL, default=scan_interval): int, + vol.Required(CONF_CODE_ARM_REQUIRED, default=code_arm_required): bool, + vol.Required( + CONF_CODE_DISARM_REQUIRED, default=code_disarm_required + ): bool, + } + ) + async def async_step_init(self, user_input=None): """Manage the options.""" if user_input is not None: - return self.async_create_entry( - title="", data={CONF_SCAN_INTERVAL: user_input[CONF_SCAN_INTERVAL]} - ) + return self.async_create_entry(title="", data=user_input) - current = self.config_entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ) - - options = vol.Schema({vol.Required(CONF_SCAN_INTERVAL, default=current): int}) - - return self.async_show_form(step_id="init", data_schema=options) + return self.async_show_form(step_id="init", data_schema=self._options_schema()) diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index 0beb3b491db..23d29bc11a9 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -5,3 +5,6 @@ DOMAIN = "risco" DATA_COORDINATOR = "risco" DEFAULT_SCAN_INTERVAL = 30 + +CONF_CODE_ARM_REQUIRED = "code_arm_required" +CONF_CODE_DISARM_REQUIRED = "code_disarm_required" diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json index 839f4d67251..32f3334d7ed 100644 --- a/homeassistant/components/risco/strings.json +++ b/homeassistant/components/risco/strings.json @@ -23,9 +23,11 @@ "init": { "title": "Configure options", "data": { - "scan_interval": "How often to poll Risco (in seconds)" + "scan_interval": "How often to poll Risco (in seconds)", + "code_arm_required": "Require pin code to arm", + "code_disarm_required": "Require pin code to disarm" } } } } -} \ No newline at end of file +} diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 46e285199ad..197ebfb8213 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -33,6 +33,8 @@ TEST_SITE_NAME = "test-site-name" FIRST_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0" SECOND_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_1" +CODES_REQUIRED_OPTIONS = {"code_arm_required": True, "code_disarm_required": True} + def _partition_mock(): return MagicMock( @@ -63,8 +65,8 @@ def two_part_alarm(): yield alarm_mock -async def _setup_risco(hass): - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) +async def _setup_risco(hass, options={}): + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, options=options) config_entry.add_to_hass(hass) with patch( @@ -191,14 +193,22 @@ async def test_states(hass, two_part_alarm): ) -async def _test_servie_call(hass, service, method, entity_id, partition_id): - with patch("homeassistant.components.risco.RiscoAPI." + method) as set_mock: - await _call_alarm_service(hass, service, entity_id) +async def _test_service_call(hass, service, method, entity_id, partition_id, **kwargs): + with patch(f"homeassistant.components.risco.RiscoAPI.{method}") as set_mock: + await _call_alarm_service(hass, service, entity_id, **kwargs) set_mock.assert_awaited_once_with(partition_id) -async def _call_alarm_service(hass, service, entity_id): - data = {"entity_id": entity_id} +async def _test_no_service_call( + hass, service, method, entity_id, partition_id, **kwargs +): + with patch(f"homeassistant.components.risco.RiscoAPI.{method}") as set_mock: + await _call_alarm_service(hass, service, entity_id, **kwargs) + set_mock.assert_not_awaited() + + +async def _call_alarm_service(hass, service, entity_id, **kwargs): + data = {"entity_id": entity_id, **kwargs} await hass.services.async_call( ALARM_DOMAIN, service, service_data=data, blocking=True @@ -209,13 +219,63 @@ async def test_sets(hass, two_part_alarm): """Test settings the various modes.""" await _setup_risco(hass) - await _test_servie_call(hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0) - await _test_servie_call(hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1) - await _test_servie_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0) - await _test_servie_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1) - await _test_servie_call( + await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0) + await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1) + await _test_service_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0) + await _test_service_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1) + await _test_service_call( hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0 ) - await _test_servie_call( + await _test_service_call( hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1 ) + + +async def test_sets_with_correct_code(hass, two_part_alarm): + """Test settings the various modes when code is required.""" + await _setup_risco(hass, CODES_REQUIRED_OPTIONS) + + code = {"code": 1234} + await _test_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0, **code + ) + await _test_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1, **code + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0, **code + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1, **code + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0, **code + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1, **code + ) + + +async def test_sets_with_incorrect_code(hass, two_part_alarm): + """Test settings the various modes when code is required and incorrect.""" + await _setup_risco(hass, CODES_REQUIRED_OPTIONS) + + code = {"code": 4321} + await _test_no_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0, **code + ) + await _test_no_service_call( + hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1, **code + ) + await _test_no_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0, **code + ) + await _test_no_service_call( + hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1, **code + ) + await _test_no_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0, **code + ) + await _test_no_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1, **code + ) diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index a3540fd1b1e..3a929c3ed3d 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -129,7 +129,11 @@ async def test_form_already_exists(hass): async def test_options_flow(hass): """Test options flow.""" - conf = {"scan_interval": 10} + conf = { + "scan_interval": 10, + "code_arm_required": True, + "code_disarm_required": True, + } entry = MockConfigEntry( domain=DOMAIN, unique_id=TEST_DATA["username"], data=TEST_DATA, From c3ad493bb71acf6704bdd5d84922b32416498125 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2020 12:38:45 -0500 Subject: [PATCH 308/862] Report usage of extract_entities by custom components (#39185) Block core usage usage of extract_entities Suggest event.async_track_template_result instead. --- homeassistant/helpers/template.py | 6 ++++ tests/helpers/test_template.py | 60 ++++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 39317d873f5..bc18a26368d 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -30,6 +30,7 @@ from homeassistant.const import ( from homeassistant.core import State, callback, split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, location as loc_helper +from homeassistant.helpers.frame import report from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as loc_util @@ -98,6 +99,11 @@ def extract_entities( variables: TemplateVarsType = None, ) -> Union[str, List[str]]: """Extract all entities for state_changed listener from template string.""" + + report( + "called template.extract_entities. Please use event.async_track_template_result instead as it can accurately handle watching entities" + ) + if template is None or not is_template_string(template): return [] diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 74cd19a165d..9d8b763c275 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -20,7 +20,14 @@ from homeassistant.helpers import template import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem -from tests.async_mock import patch +from tests.async_mock import Mock, patch + + +@pytest.fixture() +def allow_extract_entities(): + """Allow extract entities.""" + with patch("homeassistant.helpers.template.report"): + yield def _set_up_units(hass): @@ -1758,7 +1765,7 @@ def test_closest_function_no_location_states(hass): ) -def test_extract_entities_none_exclude_stuff(hass): +def test_extract_entities_none_exclude_stuff(hass, allow_extract_entities): """Test extract entities function with none or exclude stuff.""" assert template.extract_entities(hass, None) == [] @@ -1780,7 +1787,7 @@ def test_extract_entities_none_exclude_stuff(hass): ) -def test_extract_entities_no_match_entities(hass): +def test_extract_entities_no_match_entities(hass, allow_extract_entities): """Test extract entities function with none entities stuff.""" assert ( template.extract_entities( @@ -1920,7 +1927,7 @@ async def test_async_render_to_info_in_conditional(hass): assert_result_info(info, "oink", ["sensor.xyz", "sensor.pig"], []) -async def test_extract_entities_match_entities(hass): +async def test_extract_entities_match_entities(hass, allow_extract_entities): """Test extract entities function with entities stuff.""" assert ( template.extract_entities( @@ -2028,7 +2035,7 @@ states.sensor.pick_humidity.state ~ " %" ] -def test_extract_entities_with_variables(hass): +def test_extract_entities_with_variables(hass, allow_extract_entities): """Test extract entities function with variables and entities stuff.""" hass.states.async_set("input_boolean.switch", "on") assert ["input_boolean.switch"] == template.extract_entities( @@ -2063,7 +2070,7 @@ def test_extract_entities_with_variables(hass): ) -def test_extract_entities_domain_states_inner(hass): +def test_extract_entities_domain_states_inner(hass, allow_extract_entities): """Test extract entities function by domain.""" hass.states.async_set("light.switch", "on") hass.states.async_set("light.switch2", "on") @@ -2078,7 +2085,7 @@ def test_extract_entities_domain_states_inner(hass): ) == {"light.switch", "light.switch2", "light.switch3"} -def test_extract_entities_domain_states_outer(hass): +def test_extract_entities_domain_states_outer(hass, allow_extract_entities): """Test extract entities function by domain.""" hass.states.async_set("light.switch", "on") hass.states.async_set("light.switch2", "on") @@ -2093,7 +2100,7 @@ def test_extract_entities_domain_states_outer(hass): ) == {"light.switch", "light.switch2", "light.switch3"} -def test_extract_entities_domain_states_outer_with_group(hass): +def test_extract_entities_domain_states_outer_with_group(hass, allow_extract_entities): """Test extract entities function by domain.""" hass.states.async_set("light.switch", "on") hass.states.async_set("light.switch2", "on") @@ -2110,6 +2117,43 @@ def test_extract_entities_domain_states_outer_with_group(hass): ) == {"light.switch", "light.switch2", "light.switch3", "group.lights"} +def test_extract_entities_blocked_from_core_code(hass): + """Test extract entities is blocked from core code.""" + with pytest.raises(RuntimeError): + template.extract_entities( + hass, "{{ states.light }}", {}, + ) + + +def test_extract_entities_warns_and_logs_from_an_integration(hass, caplog): + """Test extract entities works from a custom_components with a log message.""" + + correct_frame = Mock( + filename="/config/custom_components/burncpu/light.py", + lineno="23", + line="self.light.is_on", + ) + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/dev/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + correct_frame, + Mock(filename="/home/dev/mdns/lights.py", lineno="2", line="something()",), + ], + ): + template.extract_entities( + hass, "{{ states.light }}", {}, + ) + + assert "custom_components/burncpu/light.py" in caplog.text + assert "23" in caplog.text + assert "self.light.is_on" in caplog.text + + def test_jinja_namespace(hass): """Test Jinja's namespace command can be used.""" test_template = template.Template( From 2e8506de80ec19b2082155890db8f10223c34e46 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Sun, 23 Aug 2020 19:58:43 +0200 Subject: [PATCH 309/862] Add unique_id to solarlog sensors (#39186) * Add unique_id to solarlog Add unique_id to solarlog sensors * Resolve suggested changes Resolve suggested changes * Update homeassistant/components/solarlog/sensor.py Ok, thanks for the explanation. Co-authored-by: Chris Talkington Co-authored-by: Chris Talkington --- homeassistant/components/solarlog/sensor.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 85ab9eb913e..ca9f2e3fc13 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass, entry, async_add_entities): # Create a new sensor for each sensor type. entities = [] for sensor_key in SENSOR_TYPES: - sensor = SolarlogSensor(platform_name, sensor_key, data) + sensor = SolarlogSensor(entry.entry_id, platform_name, sensor_key, data) entities.append(sensor) async_add_entities(entities, True) @@ -71,20 +71,28 @@ async def async_setup_entry(hass, entry, async_add_entities): class SolarlogSensor(Entity): """Representation of a Sensor.""" - def __init__(self, platform_name, sensor_key, data): + def __init__(self, entry_id, platform_name, sensor_key, data): """Initialize the sensor.""" self.platform_name = platform_name self.sensor_key = sensor_key self.data = data + self.entry_id = entry_id self._state = None self._json_key = SENSOR_TYPES[self.sensor_key][0] + self._label = SENSOR_TYPES[self.sensor_key][1] self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2] + self._icon = SENSOR_TYPES[self.sensor_key][3] + + @property + def unique_id(self): + """Return the unique id.""" + return f"{self.entry_id}_{self.sensor_key}" @property def name(self): """Return the name of the sensor.""" - return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) + return f"{self.platform_name} {self._label}" @property def unit_of_measurement(self): @@ -94,7 +102,7 @@ class SolarlogSensor(Entity): @property def icon(self): """Return the sensor icon.""" - return SENSOR_TYPES[self.sensor_key][3] + return self._icon @property def state(self): From 7462d140afb51af12941a994f3dd76ecac4661de Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 23 Aug 2020 16:20:37 -0700 Subject: [PATCH 310/862] Trim CW from RGB when not supported in ozw (#39191) * Trim the CW value if CW isn't supported * Trim CW from white level as well --- homeassistant/components/ozw/light.py | 9 +++++++-- tests/components/ozw/test_light.py | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ozw/light.py b/homeassistant/components/ozw/light.py index 209bd035fcd..b5fffbcf34f 100644 --- a/homeassistant/components/ozw/light.py +++ b/homeassistant/components/ozw/light.py @@ -204,12 +204,17 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): rgbw = "#" for colorval in color_util.color_hs_to_RGB(*hs_color): rgbw += f"{colorval:02x}" - rgbw += "0000" + if self._color_channels and self._color_channels & COLOR_CHANNEL_COLD_WHITE: + rgbw += "0000" + else: + # trim the CW value or it will not work correctly + rgbw += "00" # white LED must be off in order for color to work elif white is not None: if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - rgbw = f"#000000{white:02x}00" + # trim the CW value or it will not work correctly + rgbw = f"#000000{white:02x}" else: rgbw = f"#00000000{white:02x}" diff --git a/tests/components/ozw/test_light.py b/tests/components/ozw/test_light.py index f1055be90d3..498bf4d0cd9 100644 --- a/tests/components/ozw/test_light.py +++ b/tests/components/ozw/test_light.py @@ -377,11 +377,11 @@ async def test_pure_rgb_dimmer_light( msg = sent_messages[-2] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#ff4cff0000", "ValueIDKey": 122470423} + assert msg["payload"] == {"Value": "#ff4cff00", "ValueIDKey": 122470423} # Feedback on state light_pure_rgb_msg.decode() - light_pure_rgb_msg.payload["Value"] = "#ff4cff0000" + light_pure_rgb_msg.payload["Value"] = "#ff4cff00" light_pure_rgb_msg.encode() receive_message(light_pure_rgb_msg) await hass.async_block_till_done() @@ -500,14 +500,14 @@ async def test_no_cw_light( assert len(sent_messages) == 2 msg = sent_messages[-2] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": "#000000be00", "ValueIDKey": 659341335} + assert msg["payload"] == {"Value": "#000000be", "ValueIDKey": 659341335} # Feedback on state light_msg.decode() light_msg.payload["Value"] = byte_to_zwave_brightness(255) light_msg.encode() light_rgb_msg.decode() - light_rgb_msg.payload["Value"] = "#000000be00" + light_rgb_msg.payload["Value"] = "#000000be" light_rgb_msg.encode() receive_message(light_msg) receive_message(light_rgb_msg) From e17c87ef72129f41f1ef95be0f133c961a115b1c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 24 Aug 2020 10:45:44 +0200 Subject: [PATCH 311/862] Upgrade debugpy to 1.0.0rc2 (#39195) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index b344fbfc08f..1c77323180c 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.0.0b12"], + "requirements": ["debugpy==1.0.0rc2"], "codeowners": ["@frenck"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 45c10e0df93..d0fd51ca3f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -458,7 +458,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.0.0b12 +debugpy==1.0.0rc2 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 748e263f6d6..362ed03aec6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -237,7 +237,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.0.0b12 +debugpy==1.0.0rc2 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 71acb2c665abeb93494408fab5f39ac348ad3f98 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Aug 2020 10:54:26 +0200 Subject: [PATCH 312/862] Only reload config entry if it is loaded (#39202) --- homeassistant/config_entries.py | 6 +++++- tests/components/volumio/test_config_flow.py | 1 + tests/test_config_entries.py | 21 +++++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 90d9c623fac..9bfc9a1f1d0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -885,7 +885,11 @@ class ConfigFlow(data_entry_flow.FlowHandler): changed = self.hass.config_entries.async_update_entry( entry, data={**entry.data, **updates} ) - if changed and reload_on_update: + if ( + changed + and reload_on_update + and entry.state in (ENTRY_STATE_LOADED, ENTRY_STATE_SETUP_RETRY) + ): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index 6fc7390da79..b0532ce55d8 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -239,6 +239,7 @@ async def test_discovery_updates_unique_id(hass): "name": "dummy", "id": TEST_DISCOVERY_RESULT["id"], }, + state=config_entries.ENTRY_STATE_SETUP_RETRY, ) entry.add_to_hass(hass) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index ab28ecc7af3..644987de43c 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1169,6 +1169,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): domain="comp", data={"additional": "data", "host": "0.0.0.0"}, unique_id="mock-unique-id", + state=config_entries.ENTRY_STATE_LOADED, ) entry.add_to_hass(hass) @@ -1176,6 +1177,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): hass, MockModule("comp"), ) mock_entity_platform(hass, "config_flow.comp", None) + updates = {"host": "1.1.1.1"} class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -1186,7 +1188,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): """Test user step.""" await self.async_set_unique_id("mock-unique-id") await self._abort_if_unique_id_configured( - updates={"host": "1.1.1.1"}, reload_on_update=True + updates=updates, reload_on_update=True ) with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( @@ -1203,6 +1205,23 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): assert entry.data["additional"] == "data" assert len(async_reload.mock_calls) == 1 + # Test we don't reload if entry not started + updates["host"] = "2.2.2.2" + entry.state = config_entries.ENTRY_STATE_NOT_LOADED + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload: + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data["host"] == "2.2.2.2" + assert entry.data["additional"] == "data" + assert len(async_reload.mock_calls) == 0 + async def test_unique_id_not_update_existing_entry(hass, manager): """Test that we do not update an entry if existing entry has the data.""" From 3df67ff9e12868734b369a2a27612036638f6df4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Aug 2020 11:13:12 +0200 Subject: [PATCH 313/862] Fix race when waiting for MQTT ACK (#39193) --- homeassistant/components/mqtt/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 87258d17f99..439f37473a5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -939,10 +939,9 @@ class MQTT: self.hass.add_job(self._mqtt_handle_mid, mid) async def _mqtt_handle_mid(self, mid) -> None: - if mid in self._pending_operations: - self._pending_operations[mid].set() - else: - _LOGGER.warning("Unknown mid %d", mid) + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + self._pending_operations[mid].set() def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: """Disconnected callback.""" @@ -957,7 +956,8 @@ class MQTT: async def _wait_for_mid(self, mid): """Wait for ACK from broker.""" - self._pending_operations[mid] = asyncio.Event() + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() try: await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) except asyncio.TimeoutError: From 6d95ee7a00de1524da5e11632cc002f3790d602a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 24 Aug 2020 05:41:01 -0500 Subject: [PATCH 314/862] Websocket media browsing for Plex (#35590) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- .../components/media_player/__init__.py | 117 ++++++++++++++-- .../components/media_player/const.py | 1 + .../components/media_player/errors.py | 10 ++ .../components/plex/media_browser.py | 125 ++++++++++++++++++ homeassistant/components/plex/media_player.py | 22 ++- homeassistant/components/plex/server.py | 7 +- .../components/websocket_api/const.py | 1 + tests/components/media_player/test_init.py | 57 ++++++++ 8 files changed, 323 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/media_player/errors.py create mode 100644 homeassistant/components/plex/media_browser.py diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 24b1b570476..70eda4329c6 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -18,6 +18,11 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from homeassistant.components.websocket_api.const import ( + ERR_NOT_FOUND, + ERR_NOT_SUPPORTED, + ERR_UNKNOWN_ERROR, +) from homeassistant.const import ( HTTP_INTERNAL_SERVER_ERROR, HTTP_NOT_FOUND, @@ -84,6 +89,7 @@ from .const import ( SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -101,6 +107,7 @@ from .const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) +from .errors import BrowseError # mypy: allow-untyped-defs, no-check-untyped-defs @@ -171,12 +178,6 @@ def is_on(hass, entity_id=None): ) -WS_TYPE_MEDIA_PLAYER_THUMBNAIL = "media_player_thumbnail" -SCHEMA_WEBSOCKET_GET_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {"type": WS_TYPE_MEDIA_PLAYER_THUMBNAIL, "entity_id": cv.entity_id} -) - - def _rename_keys(**keys): """Create validator that renames keys. @@ -200,11 +201,8 @@ async def async_setup(hass, config): logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) - hass.components.websocket_api.async_register_command( - WS_TYPE_MEDIA_PLAYER_THUMBNAIL, - websocket_handle_thumbnail, - SCHEMA_WEBSOCKET_GET_THUMBNAIL, - ) + hass.components.websocket_api.async_register_command(websocket_handle_thumbnail) + hass.components.websocket_api.async_register_command(websocket_browse_media) hass.http.register_view(MediaPlayerImageView(component)) await component.async_setup(config) @@ -812,6 +810,27 @@ class MediaPlayerEntity(Entity): return state_attr + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """ + Return a payload for the "media_player/browse_media" websocket command. + + Payload should follow this format: + { + "title": str - Title of the item + "media_content_type": str - see below + "media_content_id": str - see below + - Can be passed back in to browse further + - Can be used as-is with media_player.play_media service + "can_play": bool - If item is playable + "can_expand": bool - If item contains other media + "thumbnail": str (Optional) - URL to image thumbnail for item + "children": list (Optional) - [{}, ...] + } + + Note: Children should omit the children key. + """ + raise NotImplementedError() + async def _async_fetch_image(hass, url): """Fetch image. @@ -888,6 +907,12 @@ class MediaPlayerImageView(HomeAssistantView): return web.Response(body=data, content_type=content_type, headers=headers) +@websocket_api.websocket_command( + { + vol.Required("type"): "media_player_thumbnail", + vol.Required("entity_id"): cv.entity_id, + } +) @websocket_api.async_response async def websocket_handle_thumbnail(hass, connection, msg): """Handle get media player cover command. @@ -899,9 +924,7 @@ async def websocket_handle_thumbnail(hass, connection, msg): if player is None: connection.send_message( - websocket_api.error_message( - msg["id"], "entity_not_found", "Entity not found" - ) + websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") ) return @@ -928,6 +951,72 @@ async def websocket_handle_thumbnail(hass, connection, msg): ) +@websocket_api.websocket_command( + { + vol.Required("type"): "media_player/browse_media", + vol.Required("entity_id"): cv.entity_id, + vol.Inclusive( + ATTR_MEDIA_CONTENT_TYPE, + "media_ids", + "media_content_type and media_content_id must be provided together", + ): str, + vol.Inclusive( + ATTR_MEDIA_CONTENT_ID, + "media_ids", + "media_content_type and media_content_id must be provided together", + ): str, + } +) +@websocket_api.async_response +async def websocket_browse_media(hass, connection, msg): + """ + Browse media available to the media_player entity. + + To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() + """ + component = hass.data[DOMAIN] + player = component.get_entity(msg["entity_id"]) + + if player is None: + connection.send_error(msg["id"], "entity_not_found", "Entity not found") + return + + if not player.supported_features & SUPPORT_BROWSE_MEDIA: + connection.send_message( + websocket_api.error_message( + msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media" + ) + ) + return + + media_content_type = msg.get(ATTR_MEDIA_CONTENT_TYPE) + media_content_id = msg.get(ATTR_MEDIA_CONTENT_ID) + + try: + payload = await player.async_browse_media(media_content_type, media_content_id) + except NotImplementedError: + _LOGGER.error( + "%s allows media browsing but its integration (%s) does not", + player.entity_id, + player.platform.platform_name, + ) + connection.send_message( + websocket_api.error_message( + msg["id"], + ERR_NOT_SUPPORTED, + "Integration does not support browsing media", + ) + ) + return + except BrowseError as err: + connection.send_message( + websocket_api.error_message(msg["id"], ERR_UNKNOWN_ERROR, str(err)) + ) + return + + connection.send_result(msg["id"], payload) + + class MediaPlayerDevice(MediaPlayerEntity): """ABC for media player devices (for backwards compatibility).""" diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 372b34eae45..0e7e038cdc1 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -63,3 +63,4 @@ SUPPORT_CLEAR_PLAYLIST = 8192 SUPPORT_PLAY = 16384 SUPPORT_SHUFFLE_SET = 32768 SUPPORT_SELECT_SOUND_MODE = 65536 +SUPPORT_BROWSE_MEDIA = 131072 diff --git a/homeassistant/components/media_player/errors.py b/homeassistant/components/media_player/errors.py new file mode 100644 index 00000000000..2e8443c2794 --- /dev/null +++ b/homeassistant/components/media_player/errors.py @@ -0,0 +1,10 @@ +"""Errors for the Media Player component.""" +from homeassistant.exceptions import HomeAssistantError + + +class MediaPlayerException(HomeAssistantError): + """Base class for Media Player exceptions.""" + + +class BrowseError(MediaPlayerException): + """Error while browsing.""" diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py new file mode 100644 index 00000000000..ac316edb938 --- /dev/null +++ b/homeassistant/components/plex/media_browser.py @@ -0,0 +1,125 @@ +"""Support to interface with the Plex API.""" +import logging + +from homeassistant.components.media_player.errors import BrowseError + +from .const import DOMAIN + +EXPANDABLES = ["album", "artist", "playlist", "season", "show"] +PLAYLISTS_BROWSE_PAYLOAD = { + "title": "Playlists", + "media_content_id": "all", + "media_content_type": "playlists", + "can_play": False, + "can_expand": True, +} + +_LOGGER = logging.getLogger(__name__) + + +def browse_media( + entity_id, plex_server, media_content_type=None, media_content_id=None +): + """Implement the websocket media browsing helper.""" + + def build_item_response(payload): + """Create response payload for the provided media query.""" + media = plex_server.lookup_media(**payload) + + if media is None: + return None + + media_info = item_payload(media) + if media_info.get("can_expand"): + media_info["children"] = [] + for item in media: + media_info["children"].append(item_payload(item)) + return media_info + + if ( + media_content_type == "server" + and media_content_id != plex_server.machine_identifier + ): + raise BrowseError( + f"Plex server with ID '{media_content_id}' is not associated with {entity_id}" + ) + + if media_content_type in ["server", None]: + return server_payload(plex_server) + + if media_content_type == "library": + return library_payload(plex_server, media_content_id) + + if media_content_type == "playlists": + return playlists_payload(plex_server) + + payload = { + "media_type": DOMAIN, + "plex_key": int(media_content_id), + } + response = build_item_response(payload) + if response is None: + raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}") + return response + + +def item_payload(item): + """Create response payload for a single media item.""" + payload = { + "title": item.title, + "media_content_id": str(item.ratingKey), + "media_content_type": item.type, + "can_play": True, + } + if hasattr(item, "thumbUrl"): + payload["thumbnail"] = item.thumbUrl + if item.type in EXPANDABLES: + payload["can_expand"] = True + return payload + + +def library_section_payload(section): + """Create response payload for a single library section.""" + return { + "title": section.title, + "media_content_id": section.key, + "media_content_type": "library", + "can_play": False, + "can_expand": True, + } + + +def server_payload(plex_server): + """Create response payload to describe libraries of the Plex server.""" + server_info = { + "title": plex_server.friendly_name, + "media_content_id": plex_server.machine_identifier, + "media_content_type": "server", + "can_play": False, + "can_expand": True, + } + server_info["children"] = [] + for library in plex_server.library.sections(): + if library.type == "photo": + continue + server_info["children"].append(library_section_payload(library)) + server_info["children"].append(PLAYLISTS_BROWSE_PAYLOAD) + return server_info + + +def library_payload(plex_server, library_id): + """Create response payload to describe contents of a specific library.""" + library = plex_server.library.sectionByID(library_id) + library_info = library_section_payload(library) + library_info["children"] = [] + for item in library.all(): + library_info["children"].append(item_payload(item)) + return library_info + + +def playlists_payload(plex_server): + """Create response payload for all available playlists.""" + playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []} + for playlist in plex_server.playlists(): + playlists_info["children"].append(item_payload(playlist)) + return playlists_info diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1b7db505e29..f5dc98e4eb1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -11,6 +11,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -36,8 +37,16 @@ from .const import ( PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, SERVERS, ) +from .media_browser import browse_media LIVE_TV_SECTION = "-4" +PLAYLISTS_BROWSE_PAYLOAD = { + "title": "Playlists", + "media_content_id": "all", + "media_content_type": "playlists", + "can_play": False, + "can_expand": True, +} _LOGGER = logging.getLogger(__name__) @@ -489,9 +498,10 @@ class PlexMediaPlayer(MediaPlayerEntity): | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_MUTE + | SUPPORT_BROWSE_MEDIA ) - return SUPPORT_PLAY_MEDIA + return SUPPORT_BROWSE_MEDIA | SUPPORT_PLAY_MEDIA def set_volume_level(self, volume): """Set volume level, range 0..1.""" @@ -611,3 +621,13 @@ class PlexMediaPlayer(MediaPlayerEntity): "sw_version": self._device_version, "via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier), } + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await self.hass.async_add_executor_job( + browse_media, + self.entity_id, + self.plex_server, + media_content_type, + media_content_id, + ) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 146291bdbcf..a1bf56eb54f 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -31,7 +31,6 @@ from .const import ( CONF_USE_EPISODE_ART, DEBOUNCE_TIMEOUT, DEFAULT_VERIFY_SSL, - DOMAIN, PLAYER_SOURCE, PLEX_NEW_MP_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, @@ -449,6 +448,10 @@ class PlexServer: """Return playlist from server object.""" return self._plex_server.playlist(title) + def playlists(self): + """Return available playlists from server object.""" + return self._plex_server.playlists() + def create_playqueue(self, media, **kwargs): """Create playqueue on Plex server.""" return plexapi.playqueue.PlayQueue.create(self._plex_server, media, **kwargs) @@ -461,7 +464,7 @@ class PlexServer: """Lookup a piece of media.""" media_type = media_type.lower() - if media_type == DOMAIN: + if isinstance(kwargs.get("plex_key"), int): key = kwargs["plex_key"] try: return self.fetch_item(key) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 121ea7496de..f01a2880b9d 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -23,6 +23,7 @@ MAX_PENDING_MSG = 2048 ERR_ID_REUSE = "id_reuse" ERR_INVALID_FORMAT = "invalid_format" ERR_NOT_FOUND = "not_found" +ERR_NOT_SUPPORTED = "not_supported" ERR_HOME_ASSISTANT_ERROR = "home_assistant_error" ERR_UNKNOWN_COMMAND = "unknown_command" ERR_UNKNOWN_ERROR = "unknown_error" diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index d5eac466093..02012a1f71d 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -100,3 +100,60 @@ def test_deprecated_base_class(caplog): CustomMediaPlayer() assert "MediaPlayerDevice is deprecated, modify CustomMediaPlayer" in caplog.text + + +async def test_media_browse(hass, hass_ws_client): + """Test browsing media.""" + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", + media_player.SUPPORT_BROWSE_MEDIA, + ), patch( + "homeassistant.components.media_player.MediaPlayerEntity." "async_browse_media", + return_value={"bla": "yo"}, + ) as mock_browse_media: + await client.send_json( + { + "id": 5, + "type": "media_player/browse_media", + "entity_id": "media_player.bedroom", + "media_content_type": "album", + "media_content_id": "abcd", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert msg["result"] == {"bla": "yo"} + assert mock_browse_media.mock_calls[0][1] == ("album", "abcd") + + with patch( + "homeassistant.components.demo.media_player.YOUTUBE_PLAYER_SUPPORT", + media_player.SUPPORT_BROWSE_MEDIA, + ), patch( + "homeassistant.components.media_player.MediaPlayerEntity." "async_browse_media", + return_value={"bla": "yo"}, + ): + await client.send_json( + { + "id": 6, + "type": "media_player/browse_media", + "entity_id": "media_player.bedroom", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 6 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert msg["result"] == {"bla": "yo"} From ca2bc9906d72e1bf1ab058df3c2e4492f5ecdcb8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Aug 2020 12:43:31 +0200 Subject: [PATCH 315/862] Add Shelly integration (#39178) --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/shelly/__init__.py | 187 ++++++++++++++++++ .../components/shelly/config_flow.py | 129 ++++++++++++ homeassistant/components/shelly/const.py | 3 + homeassistant/components/shelly/manifest.json | 9 + homeassistant/components/shelly/strings.json | 24 +++ homeassistant/components/shelly/switch.py | 71 +++++++ .../components/shelly/translations/en.json | 24 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 3 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/shelly/__init__.py | 1 + tests/components/shelly/test_config_flow.py | 162 +++++++++++++++ tests/test_config_entries.py | 1 + 16 files changed, 624 insertions(+) create mode 100644 homeassistant/components/shelly/__init__.py create mode 100644 homeassistant/components/shelly/config_flow.py create mode 100644 homeassistant/components/shelly/const.py create mode 100644 homeassistant/components/shelly/manifest.json create mode 100644 homeassistant/components/shelly/strings.json create mode 100644 homeassistant/components/shelly/switch.py create mode 100644 homeassistant/components/shelly/translations/en.json create mode 100644 tests/components/shelly/__init__.py create mode 100644 tests/components/shelly/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index bccf6bbf2ea..8c67762250e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -754,6 +754,8 @@ omit = homeassistant/components/seventeentrack/sensor.py homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py + homeassistant/components/shelly/__init__.py + homeassistant/components/shelly/switch.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py homeassistant/components/simplepush/notify.py diff --git a/CODEOWNERS b/CODEOWNERS index de2fae6e532..d91591a4526 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -367,6 +367,7 @@ homeassistant/components/serial/* @fabaff homeassistant/components/seven_segments/* @fabaff homeassistant/components/seventeentrack/* @bachya homeassistant/components/shell_command/* @home-assistant/core +homeassistant/components/shelly/* @balloob homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/sighthound/* @robmarkcole diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py new file mode 100644 index 00000000000..4829aeb49f2 --- /dev/null +++ b/homeassistant/components/shelly/__init__.py @@ -0,0 +1,187 @@ +"""The Shelly integration.""" +import asyncio +from datetime import timedelta +import logging + +from aiocoap import error as aiocoap_error +import aioshelly +import async_timeout + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import ( + aiohttp_client, + device_registry, + entity, + update_coordinator, +) + +from .const import DOMAIN + +PLATFORMS = ["switch"] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Shelly component.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Shelly from a config entry.""" + try: + async with async_timeout.timeout(5): + device = await aioshelly.Device.create( + entry.data["host"], aiohttp_client.async_get_clientsession(hass) + ) + except (asyncio.TimeoutError, OSError): + raise ConfigEntryNotReady + + wrapper = hass.data[DOMAIN][entry.entry_id] = ShellyDeviceWrapper( + hass, entry, device + ) + await wrapper.async_setup() + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): + """Wrapper for a Shelly device with Home Assistant specific functions.""" + + def __init__(self, hass, entry, device: aioshelly.Device): + """Initialize the Shelly device wrapper.""" + super().__init__( + hass, + _LOGGER, + name=device.settings["name"] or entry.title, + update_interval=timedelta(seconds=5), + ) + self.hass = hass + self.entry = entry + self.device = device + self._unsub_stop = None + + async def _async_update_data(self): + """Fetch data.""" + # Race condition on shutdown. Stop all the fetches. + if self._unsub_stop is None: + return None + + try: + async with async_timeout.timeout(5): + return await self.device.update() + except aiocoap_error.Error: + raise update_coordinator.UpdateFailed("Error fetching data") + + @property + def model(self): + """Model of the device.""" + return self.device.settings["device"]["type"] + + @property + def mac(self): + """Mac address of the device.""" + return self.device.settings["device"]["mac"] + + async def async_setup(self): + """Set up the wrapper.""" + self._unsub_stop = self.hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop + ) + dev_reg = await device_registry.async_get_registry(self.hass) + model_type = self.device.settings["device"]["type"] + dev_reg.async_get_or_create( + config_entry_id=self.entry.entry_id, + name=self.name, + connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, + # This is duplicate but otherwise via_device can't work + identifiers={(DOMAIN, self.mac)}, + manufacturer="Shelly", + model=aioshelly.MODEL_NAMES.get(model_type, model_type), + sw_version=self.device.settings["fw"], + ) + + async def shutdown(self): + """Shutdown the device wrapper.""" + if self._unsub_stop: + self._unsub_stop() + self._unsub_stop = None + await self.device.shutdown() + + async def _handle_ha_stop(self, _): + """Handle Home Assistant stopping.""" + self._unsub_stop = None + await self.shutdown() + + +class ShellyBlockEntity(entity.Entity): + """Helper class to represent a block.""" + + def __init__(self, wrapper: ShellyDeviceWrapper, block): + """Initialize Shelly entity.""" + self.wrapper = wrapper + self.block = block + + @property + def name(self): + """Name of entity.""" + return f"{self.wrapper.name} - {self.block.description}" + + @property + def should_poll(self): + """If device should be polled.""" + return False + + @property + def device_info(self): + """Device info.""" + return { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + } + + @property + def available(self): + """Available.""" + return self.wrapper.last_update_success + + @property + def unique_id(self): + """Return unique ID of entity.""" + return f"{self.wrapper.mac}-{self.block.index}" + + async def async_added_to_hass(self): + """When entity is added to HASS.""" + self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) + + async def async_update(self): + """Update entity with latest info.""" + await self.wrapper.async_request_refresh() + + @callback + def _update_callback(self): + """Handle device update.""" + self.async_write_ha_state() + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + await hass.data[DOMAIN].pop(entry.entry_id).shutdown() + + return unload_ok diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py new file mode 100644 index 00000000000..c58bcf8c89a --- /dev/null +++ b/homeassistant/components/shelly/config_flow.py @@ -0,0 +1,129 @@ +"""Config flow for Shelly integration.""" +import asyncio +import logging + +import aiohttp +import aioshelly +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({"host": str}) + +HTTP_CONNECT_ERRORS = (asyncio.TimeoutError, aiohttp.ClientError) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + async with async_timeout.timeout(5): + device = await aioshelly.Device.create( + data["host"], aiohttp_client.async_get_clientsession(hass) + ) + + await device.shutdown() + + # Return info that you want to store in the config entry. + return {"title": device.settings["name"], "mac": device.settings["device"]["mac"]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Shelly.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + host = None + info = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await self._async_get_info(user_input["host"]) + except HTTP_CONNECT_ERRORS: + errors["base"] = "cannot_connect" + + else: + if info["auth"]: + return self.async_abort(reason="auth_not_supported") + + try: + device_info = await validate_input(self.hass, user_input) + except asyncio.TimeoutError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(device_info["mac"]) + return self.async_create_entry( + title=device_info["title"] or user_input["host"], + data=user_input, + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_zeroconf(self, zeroconf_info): + """Handle zeroconf discovery.""" + if not zeroconf_info.get("name", "").startswith("shelly"): + return self.async_abort(reason="not_shelly") + + try: + self.info = info = await self._async_get_info(zeroconf_info["host"]) + except HTTP_CONNECT_ERRORS: + return self.async_abort(reason="cannot_connect") + + if info["auth"]: + return self.async_abort(reason="auth_not_supported") + + await self.async_set_unique_id(info["mac"]) + self._abort_if_unique_id_configured({"host": zeroconf_info["host"]}) + self.host = zeroconf_info["host"] + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = {"name": zeroconf_info["host"]} + return await self.async_step_confirm_discovery() + + async def async_step_confirm_discovery(self, user_input=None): + """Handle discovery confirm.""" + errors = {} + if user_input is not None: + try: + device_info = await validate_input(self.hass, {"host": self.host}) + except asyncio.TimeoutError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=device_info["title"] or self.host, data={"host": self.host} + ) + + return self.async_show_form( + step_id="confirm_discovery", + description_placeholders={ + "model": aioshelly.MODEL_NAMES.get( + self.info["type"], self.info["type"] + ), + "host": self.host, + }, + errors=errors, + ) + + async def _async_get_info(self, host): + """Get info from shelly device.""" + async with async_timeout.timeout(5): + return await aioshelly.get_info( + aiohttp_client.async_get_clientsession(self.hass), host, + ) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py new file mode 100644 index 00000000000..5c9a55915fe --- /dev/null +++ b/homeassistant/components/shelly/const.py @@ -0,0 +1,3 @@ +"""Constants for the Shelly integration.""" + +DOMAIN = "shelly" diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json new file mode 100644 index 00000000000..149b8ef18cd --- /dev/null +++ b/homeassistant/components/shelly/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "shelly", + "name": "Shelly", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/shelly2", + "requirements": ["aioshelly==0.1.2"], + "zeroconf": ["_http._tcp.local."], + "codeowners": ["@balloob"] +} diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json new file mode 100644 index 00000000000..9c5f5707914 --- /dev/null +++ b/homeassistant/components/shelly/strings.json @@ -0,0 +1,24 @@ +{ + "title": "Shelly", + "config": { + "flow_title": "Shelly: {name}", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm_discovery": { + "description": "Do you want to set up the {model} at {host}?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "auth_not_supported": "Shelly devices requiring authentication are not currently supported." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py new file mode 100644 index 00000000000..4a6c2a21b0b --- /dev/null +++ b/homeassistant/components/shelly/switch.py @@ -0,0 +1,71 @@ +"""Switch for Shelly.""" +from homeassistant.components.shelly import ShellyBlockEntity +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import callback + +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up switches for device.""" + wrapper = hass.data[DOMAIN][config_entry.entry_id] + relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"] + + if not relay_blocks: + return + + if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": + return + + multiple_blocks = len(relay_blocks) > 1 + async_add_entities( + RelaySwitch(wrapper, block, multiple_blocks=multiple_blocks) + for block in relay_blocks + ) + + +class RelaySwitch(ShellyBlockEntity, SwitchEntity): + """Switch that controls a relay block on Shelly devices.""" + + def __init__(self, *args, multiple_blocks) -> None: + """Initialize relay switch.""" + super().__init__(*args) + self.multiple_blocks = multiple_blocks + self.control_result = None + + @property + def is_on(self) -> bool: + """If switch is on.""" + if self.control_result: + return self.control_result["ison"] + + return self.block.output + + @property + def device_info(self): + """Device info.""" + if not self.multiple_blocks: + return super().device_info + + # If a device has multiple relays, we want to expose as separate device + return { + "name": self.name, + "identifiers": {(DOMAIN, self.wrapper.mac, self.block.index)}, + "via_device": (DOMAIN, self.wrapper.mac), + } + + async def async_turn_on(self, **kwargs): + """Turn on relay.""" + self.control_result = await self.block.turn_on() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn off relay.""" + self.control_result = await self.block.turn_off() + self.async_write_ha_state() + + @callback + def _update_callback(self): + """When device updates, clear control result that overrides state.""" + self.control_result = None + super()._update_callback() diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json new file mode 100644 index 00000000000..89660bbda1a --- /dev/null +++ b/homeassistant/components/shelly/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "auth_not_supported": "Authenticated Shelly devices are not currently supported.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "Do you want to set up the {model} at {host}?" + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e1ab7647446..b5db34ec485 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -151,6 +151,7 @@ FLOWS = [ "samsungtv", "sense", "sentry", + "shelly", "shopping_list", "simplisafe", "smappee", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 7779fbb155e..ba12b4ec4de 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -37,6 +37,9 @@ ZEROCONF = { "_hap._tcp.local.": [ "homekit_controller" ], + "_http._tcp.local.": [ + "shelly" + ], "_ipp._tcp.local.": [ "ipp" ], diff --git a/requirements_all.txt b/requirements_all.txt index d0fd51ca3f5..153cb8117ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,6 +221,9 @@ aiopvpc==2.0.2 # homeassistant.components.webostv aiopylgtv==0.3.3 +# homeassistant.components.shelly +aioshelly==0.1.2 + # homeassistant.components.switcher_kis aioswitcher==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 362ed03aec6..d3f45827bc4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,6 +131,9 @@ aiopvpc==2.0.2 # homeassistant.components.webostv aiopylgtv==0.3.3 +# homeassistant.components.shelly +aioshelly==0.1.2 + # homeassistant.components.switcher_kis aioswitcher==1.2.0 diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py new file mode 100644 index 00000000000..3c502c81deb --- /dev/null +++ b/tests/components/shelly/__init__.py @@ -0,0 +1 @@ +"""Tests for the Shelly integration.""" diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py new file mode 100644 index 00000000000..c80397d9150 --- /dev/null +++ b/tests/components/shelly/test_config_flow.py @@ -0,0 +1,162 @@ +"""Test the Shelly config flow.""" +import asyncio + +from homeassistant import config_entries, setup +from homeassistant.components.shelly.const import DOMAIN + +from tests.async_mock import AsyncMock, Mock, 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( + "aioshelly.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ), patch( + "aioshelly.Device.create", + return_value=Mock( + shutdown=AsyncMock(), + settings={"name": "Test name", "device": {"mac": "test-mac"}}, + ), + ), patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.shelly.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test name" + assert result2["data"] == { + "host": "1.1.1.1", + } + 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_auth(hass): + """Test we can't manually configure if auth is required.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "aioshelly.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "auth_not_supported" + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "aioshelly.get_info", side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_zeroconf(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "aioshelly.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": "1.1.1.1", "name": "shelly1pm-12345"}, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "aioshelly.Device.create", + return_value=Mock( + shutdown=AsyncMock(), + settings={"name": "Test name", "device": {"mac": "test-mac"}}, + ), + ), patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.shelly.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test name" + assert result2["data"] == { + "host": "1.1.1.1", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_already_configured(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0"} + ) + entry.add_to_hass(hass) + + with patch( + "aioshelly.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": "1.1.1.1", "name": "shelly1pm-12345"}, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + +async def test_zeroconf_require_auth(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "aioshelly.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": "1.1.1.1", "name": "shelly1pm-12345"}, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "abort" + assert result["reason"] == "auth_not_supported" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 644987de43c..988e67718ef 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1127,6 +1127,7 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager): domain="comp", data={"additional": "data", "host": "0.0.0.0"}, unique_id="mock-unique-id", + state=config_entries.ENTRY_STATE_LOADED, ) entry.add_to_hass(hass) From 46ab5ab38e2d4dd834b18b213ace93459114c2ad Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 24 Aug 2020 12:47:55 +0200 Subject: [PATCH 316/862] Upgrade pre-commit to 2.7.1 (#39206) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index c5b01a3898c..094ff1a00c6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ codecov==2.1.0 coverage==5.2.1 mock-open==1.4.0 mypy==0.780 -pre-commit==2.7.0 +pre-commit==2.7.1 pylint==2.4.4 astroid==2.3.3 pylint-strict-informational==0.1 From e61f7f02742a3dc5acf459e1ef075be12e299f44 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Aug 2020 13:39:23 +0200 Subject: [PATCH 317/862] 100% test coverage for Shelly config flow (#39209) --- .../components/shelly/config_flow.py | 3 + tests/components/shelly/test_config_flow.py | 89 ++++++++++++++++++- 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index c58bcf8c89a..da57a94e628 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -51,6 +51,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): info = await self._async_get_info(user_input["host"]) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" else: if info["auth"]: diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index c80397d9150..8a3b7f48fcf 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Shelly config flow.""" import asyncio +import pytest + from homeassistant import config_entries, setup from homeassistant.components.shelly.const import DOMAIN @@ -65,21 +67,46 @@ async def test_form_auth(hass): assert result2["reason"] == "auth_not_supported" -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" +@pytest.mark.parametrize( + "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] +) +async def test_form_errors_get_info(hass, error): + """Test we handle errors.""" + exc, base_error = error result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "aioshelly.get_info", side_effect=asyncio.TimeoutError, + "aioshelly.get_info", side_effect=exc, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": base_error} + + +@pytest.mark.parametrize( + "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] +) +async def test_form_errors_test_connection(hass, error): + """Test we handle errors.""" + exc, base_error = error + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aioshelly.get_info", return_value={"auth": False}), patch( + "aioshelly.Device.create", side_effect=exc, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": base_error} async def test_zeroconf(hass): @@ -121,6 +148,35 @@ async def test_zeroconf(hass): assert len(mock_setup_entry.mock_calls) == 1 +@pytest.mark.parametrize( + "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] +) +async def test_zeroconf_confirm_error(hass, error): + """Test we get the form.""" + exc, base_error = error + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "aioshelly.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": "1.1.1.1", "name": "shelly1pm-12345"}, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "aioshelly.Device.create", side_effect=exc, + ): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": base_error} + + async def test_zeroconf_already_configured(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -145,6 +201,20 @@ async def test_zeroconf_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_zeroconf_cannot_connect(hass): + """Test we get the form.""" + with patch( + "aioshelly.get_info", side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": "1.1.1.1", "name": "shelly1pm-12345"}, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + async def test_zeroconf_require_auth(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -160,3 +230,14 @@ async def test_zeroconf_require_auth(hass): ) assert result["type"] == "abort" assert result["reason"] == "auth_not_supported" + + +async def test_zeroconf_not_shelly(hass): + """Test we filter out non-shelly devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": "1.1.1.1", "name": "notshelly"}, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "abort" + assert result["reason"] == "not_shelly" From fefa1a7259018ce8fdd7f630ba49be126ccb51af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Aug 2020 13:40:34 +0200 Subject: [PATCH 318/862] Add shortcuts when we know template is static (#39208) Co-authored-by: Franck Nijhof --- homeassistant/helpers/template.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index bc18a26368d..2403967e3cf 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -213,6 +213,7 @@ class Template: self._compiled_code = None self._compiled = None self.hass = hass + self.is_static = not is_template_string(template) @property def _env(self): @@ -237,10 +238,16 @@ class Template: self, variables: TemplateVarsType = None ) -> Union[str, List[str]]: """Extract all entities for state_changed listener.""" + if self.is_static: + return [] + return extract_entities(self.hass, self.template, variables) def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str: """Render given template.""" + if self.is_static: + return self.template + if variables is not None: kwargs.update(variables) @@ -254,6 +261,9 @@ class Template: This method must be run in the event loop. """ + if self.is_static: + return self.template + compiled = self._compiled or self._ensure_compiled() if variables is not None: @@ -270,18 +280,23 @@ class Template: ) -> RenderInfo: """Render the template and collect an entity filter.""" assert self.hass and _RENDER_INFO not in self.hass.data - render_info = self.hass.data[_RENDER_INFO] = RenderInfo(self) + + render_info = RenderInfo(self) + # pylint: disable=protected-access + if self.is_static: + render_info._freeze_static() + return render_info + + self.hass.data[_RENDER_INFO] = render_info try: render_info._result = self.async_render(variables, **kwargs) except TemplateError as ex: render_info.exception = ex finally: del self.hass.data[_RENDER_INFO] - if not is_template_string(self.template): - render_info._freeze_static() - else: - render_info._freeze() + + render_info._freeze() return render_info def render_with_possible_json_value(self, value, error_value=_SENTINEL): @@ -289,6 +304,9 @@ class Template: If valid JSON will expose value_json too. """ + if self.is_static: + return self.template + return run_callback_threadsafe( self.hass.loop, self.async_render_with_possible_json_value, @@ -306,6 +324,9 @@ class Template: This method must be run in the event loop. """ + if self.is_static: + return self.template + if self._compiled is None: self._ensure_compiled() From a47f73244c0b3fbe4210ffd7731be69b3984fbbd Mon Sep 17 00:00:00 2001 From: Leonardo Figueiro Date: Mon, 24 Aug 2020 09:15:07 -0300 Subject: [PATCH 319/862] Add Wilight integration with SSDP (#36694) Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + homeassistant/components/wilight/__init__.py | 125 ++++++ .../components/wilight/config_flow.py | 106 +++++ homeassistant/components/wilight/const.py | 14 + homeassistant/components/wilight/light.py | 179 +++++++++ .../components/wilight/manifest.json | 14 + .../components/wilight/parent_device.py | 102 +++++ homeassistant/components/wilight/strings.json | 16 + .../components/wilight/translations/en.json | 16 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 5 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/wilight/__init__.py | 83 ++++ tests/components/wilight/test_config_flow.py | 157 ++++++++ tests/components/wilight/test_init.py | 65 +++ tests/components/wilight/test_light.py | 369 ++++++++++++++++++ 17 files changed, 1259 insertions(+) create mode 100644 homeassistant/components/wilight/__init__.py create mode 100644 homeassistant/components/wilight/config_flow.py create mode 100644 homeassistant/components/wilight/const.py create mode 100644 homeassistant/components/wilight/light.py create mode 100644 homeassistant/components/wilight/manifest.json create mode 100644 homeassistant/components/wilight/parent_device.py create mode 100644 homeassistant/components/wilight/strings.json create mode 100644 homeassistant/components/wilight/translations/en.json create mode 100644 tests/components/wilight/__init__.py create mode 100644 tests/components/wilight/test_config_flow.py create mode 100644 tests/components/wilight/test_init.py create mode 100644 tests/components/wilight/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index d91591a4526..eb9c736bc5e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -474,6 +474,7 @@ homeassistant/components/weather/* @fabaff homeassistant/components/webostv/* @bendavid homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wiffi/* @mampfes +homeassistant/components/wilight/* @leofig-rj homeassistant/components/withings/* @vangorra homeassistant/components/wled/* @frenck homeassistant/components/wolflink/* @adamkrol93 diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py new file mode 100644 index 00000000000..8821190bd32 --- /dev/null +++ b/homeassistant/components/wilight/__init__.py @@ -0,0 +1,125 @@ +"""The WiLight integration.""" +import asyncio + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN +from .parent_device import WiLightParent + +# List the platforms that you want to support. +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the WiLight with Config Flow component.""" + + hass.data[DOMAIN] = {} + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up a wilight config entry.""" + + parent = WiLightParent(hass, entry) + + if not await parent.async_setup(): + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = parent + + # Set up all platforms for this device/entry. + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload WiLight config entry.""" + + # Unload entities for this entry/device. + await asyncio.gather( + *( + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ) + ) + + # Cleanup + parent = hass.data[DOMAIN][entry.entry_id] + await parent.async_reset() + del hass.data[DOMAIN][entry.entry_id] + + return True + + +class WiLightDevice(Entity): + """Representation of a WiLight device. + + Contains the common logic for WiLight entities. + """ + + def __init__(self, api_device, index, item_name): + """Initialize the device.""" + # WiLight specific attributes for every component type + self._device_id = api_device.device_id + self._sw_version = api_device.swversion + self._client = api_device.client + self._model = api_device.model + self._name = item_name + self._index = index + self._unique_id = f"{self._device_id}_{self._index}" + self._status = {} + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return a name for this WiLight item.""" + return self._name + + @property + def unique_id(self): + """Return the unique ID for this WiLight item.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + return { + "name": self._name, + "identifiers": {(DOMAIN, self._unique_id)}, + "model": self._model, + "manufacturer": "WiLight", + "sw_version": self._sw_version, + "via_device": (DOMAIN, self._device_id), + } + + @property + def available(self): + """Return True if entity is available.""" + return bool(self._client.is_connected) + + @callback + def handle_event_callback(self, states): + """Propagate changes through ha.""" + self._status = states + self.async_write_ha_state() + + async def async_update(self): + """Synchronize state with api_device.""" + await self._client.status(self._index) + + async def async_added_to_hass(self): + """Register update callback.""" + self._client.register_status_callback(self.handle_event_callback, self._index) + await self._client.status(self._index) diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py new file mode 100644 index 00000000000..724bbb6547d --- /dev/null +++ b/homeassistant/components/wilight/config_flow.py @@ -0,0 +1,106 @@ +"""Config flow to configure WiLight.""" +import logging +from urllib.parse import urlparse + +import pywilight + +from homeassistant.components import ssdp +from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow +from homeassistant.const import CONF_HOST + +from .const import DOMAIN # pylint: disable=unused-import + +CONF_SERIAL_NUMBER = "serial_number" +CONF_MODEL_NAME = "model_name" + +WILIGHT_MANUFACTURER = "All Automacao Ltda" + +# List the components supported by this integration. +ALLOWED_WILIGHT_COMPONENTS = ["light"] + +_LOGGER = logging.getLogger(__name__) + + +class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a WiLight config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the WiLight flow.""" + self._host = None + self._serial_number = None + self._title = None + self._model_name = None + self._wilight_components = [] + self._components_text = "" + + def _wilight_update(self, host, serial_number, model_name): + self._host = host + self._serial_number = serial_number + self._title = f"WL{serial_number}" + self._model_name = model_name + self._wilight_components = pywilight.get_components_from_model(model_name) + self._components_text = ", ".join(self._wilight_components) + return self._components_text != "" + + def _get_entry(self): + data = { + CONF_HOST: self._host, + CONF_SERIAL_NUMBER: self._serial_number, + CONF_MODEL_NAME: self._model_name, + } + return self.async_create_entry(title=self._title, data=data) + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered WiLight.""" + # Filter out basic information + if ( + ssdp.ATTR_SSDP_LOCATION not in discovery_info + or ssdp.ATTR_UPNP_MANUFACTURER not in discovery_info + or ssdp.ATTR_UPNP_SERIAL not in discovery_info + or ssdp.ATTR_UPNP_MODEL_NAME not in discovery_info + or ssdp.ATTR_UPNP_MODEL_NUMBER not in discovery_info + ): + return self.async_abort(reason="not_wilight_device") + # Filter out non-WiLight devices + if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER] != WILIGHT_MANUFACTURER: + return self.async_abort(reason="not_wilight_device") + + host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname + serial_number = discovery_info[ssdp.ATTR_UPNP_SERIAL] + model_name = discovery_info[ssdp.ATTR_UPNP_MODEL_NAME] + + if not self._wilight_update(host, serial_number, model_name): + return self.async_abort(reason="not_wilight_device") + + # Check if all components of this WiLight are allowed in this version of the HA integration + component_ok = all( + wilight_component in ALLOWED_WILIGHT_COMPONENTS + for wilight_component in self._wilight_components + ) + + if not component_ok: + return self.async_abort(reason="not_supported_device") + + await self.async_set_unique_id(self._serial_number) + self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = {"name": self._title} + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of discovered WiLight.""" + if user_input is not None: + return self._get_entry() + + return self.async_show_form( + step_id="confirm", + description_placeholders={ + "name": self._title, + "components": self._components_text, + }, + errors={}, + ) diff --git a/homeassistant/components/wilight/const.py b/homeassistant/components/wilight/const.py new file mode 100644 index 00000000000..a3d77da44ef --- /dev/null +++ b/homeassistant/components/wilight/const.py @@ -0,0 +1,14 @@ +"""Constants for the WiLight integration.""" + +DOMAIN = "wilight" + +# Item types +ITEM_LIGHT = "light" + +# Light types +LIGHT_ON_OFF = "light_on_off" +LIGHT_DIMMER = "light_dimmer" +LIGHT_COLOR = "light_rgb" + +# Light service support +SUPPORT_NONE = 0 diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py new file mode 100644 index 00000000000..e4bf504165d --- /dev/null +++ b/homeassistant/components/wilight/light.py @@ -0,0 +1,179 @@ +"""Support for WiLight lights.""" + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import WiLightDevice +from .const import ( + DOMAIN, + ITEM_LIGHT, + LIGHT_COLOR, + LIGHT_DIMMER, + LIGHT_ON_OFF, + SUPPORT_NONE, +) + + +def entities_from_discovered_wilight(hass, api_device): + """Parse configuration and add WiLight light entities.""" + entities = [] + for item in api_device.items: + if item["type"] != ITEM_LIGHT: + continue + index = item["index"] + item_name = item["name"] + if item["sub_type"] == LIGHT_ON_OFF: + entity = WiLightLightOnOff(api_device, index, item_name) + elif item["sub_type"] == LIGHT_DIMMER: + entity = WiLightLightDimmer(api_device, index, item_name) + elif item["sub_type"] == LIGHT_COLOR: + entity = WiLightLightColor(api_device, index, item_name) + else: + continue + entities.append(entity) + + return entities + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up WiLight lights from a config entry.""" + parent = hass.data[DOMAIN][entry.entry_id] + + # Handle a discovered WiLight device. + entities = entities_from_discovered_wilight(hass, parent.api) + async_add_entities(entities) + + +class WiLightLightOnOff(WiLightDevice, LightEntity): + """Representation of a WiLights light on-off.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_NONE + + @property + def is_on(self): + """Return true if device is on.""" + return self._status.get("on") + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._client.turn_on(self._index) + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._client.turn_off(self._index) + + +class WiLightLightDimmer(WiLightDevice, LightEntity): + """Representation of a WiLights light dimmer.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int(self._status.get("brightness", 0)) + + @property + def is_on(self): + """Return true if device is on.""" + return self._status.get("on") + + async def async_turn_on(self, **kwargs): + """Turn the device on,set brightness if needed.""" + # Dimmer switches use a range of [0, 255] to control + # brightness. Level 255 might mean to set it to previous value + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + await self._client.set_brightness(self._index, brightness) + else: + await self._client.turn_on(self._index) + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._client.turn_off(self._index) + + +def wilight_to_hass_hue(value): + """Convert wilight hue 1..255 to hass 0..360 scale.""" + return min(360, round((value * 360) / 255, 3)) + + +def hass_to_wilight_hue(value): + """Convert hass hue 0..360 to wilight 1..255 scale.""" + return min(255, round((value * 255) / 360)) + + +def wilight_to_hass_saturation(value): + """Convert wilight saturation 1..255 to hass 0..100 scale.""" + return min(100, round((value * 100) / 255, 3)) + + +def hass_to_wilight_saturation(value): + """Convert hass saturation 0..100 to wilight 1..255 scale.""" + return min(255, round((value * 255) / 100)) + + +class WiLightLightColor(WiLightDevice, LightEntity): + """Representation of a WiLights light rgb.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int(self._status.get("brightness", 0)) + + @property + def hs_color(self): + """Return the hue and saturation color value [float, float].""" + return [ + wilight_to_hass_hue(int(self._status.get("hue", 0))), + wilight_to_hass_saturation(int(self._status.get("saturation", 0))), + ] + + @property + def is_on(self): + """Return true if device is on.""" + return self._status.get("on") + + async def async_turn_on(self, **kwargs): + """Turn the device on,set brightness if needed.""" + # Brightness use a range of [0, 255] to control + # Hue use a range of [0, 360] to control + # Saturation use a range of [0, 100] to control + if ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + hue = hass_to_wilight_hue(kwargs[ATTR_HS_COLOR][0]) + saturation = hass_to_wilight_saturation(kwargs[ATTR_HS_COLOR][1]) + await self._client.set_hsb_color(self._index, hue, saturation, brightness) + elif ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR not in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + await self._client.set_brightness(self._index, brightness) + elif ATTR_BRIGHTNESS not in kwargs and ATTR_HS_COLOR in kwargs: + hue = hass_to_wilight_hue(kwargs[ATTR_HS_COLOR][0]) + saturation = hass_to_wilight_saturation(kwargs[ATTR_HS_COLOR][1]) + await self._client.set_hs_color(self._index, hue, saturation) + else: + await self._client.turn_on(self._index) + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._client.turn_off(self._index) diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json new file mode 100644 index 00000000000..bb20da2b1ce --- /dev/null +++ b/homeassistant/components/wilight/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "wilight", + "name": "WiLight", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/wilight", + "requirements": ["pywilight==0.0.65"], + "ssdp": [ + { + "manufacturer": "All Automacao Ltda" + } + ], + "codeowners": ["@leofig-rj"], + "quality_scale": "silver" +} diff --git a/homeassistant/components/wilight/parent_device.py b/homeassistant/components/wilight/parent_device.py new file mode 100644 index 00000000000..9c2c0c1a1de --- /dev/null +++ b/homeassistant/components/wilight/parent_device.py @@ -0,0 +1,102 @@ +"""The WiLight Device integration.""" +import asyncio +import logging + +import pywilight +import requests + +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send + +_LOGGER = logging.getLogger(__name__) + + +class WiLightParent: + """Manages a single WiLight Parent Device.""" + + def __init__(self, hass, config_entry): + """Initialize the system.""" + self._host = config_entry.data[CONF_HOST] + self._hass = hass + self._api = None + + @property + def host(self): + """Return the host of this parent.""" + return self._host + + @property + def api(self): + """Return the api of this parent.""" + return self._api + + async def async_setup(self): + """Set up a WiLight Parent Device based on host parameter.""" + host = self._host + hass = self._hass + + api_device = await hass.async_add_executor_job(create_api_device, host) + + if api_device is None: + return False + + @callback + def disconnected(): + # Schedule reconnect after connection has been lost. + _LOGGER.warning("WiLight %s disconnected", api_device.device_id) + async_dispatcher_send( + hass, f"wilight_device_available_{api_device.device_id}", False + ) + + @callback + def reconnected(): + # Schedule reconnect after connection has been lost. + _LOGGER.warning("WiLight %s reconnect", api_device.device_id) + async_dispatcher_send( + hass, f"wilight_device_available_{api_device.device_id}", True + ) + + async def connect(api_device): + # Set up connection and hook it into HA for reconnect/shutdown. + _LOGGER.debug("Initiating connection to %s", api_device.device_id) + + client = await api_device.config_client( + disconnect_callback=disconnected, + reconnect_callback=reconnected, + loop=asyncio.get_running_loop(), + logger=_LOGGER, + ) + + # handle shutdown of WiLight asyncio transport + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, lambda x: client.stop() + ) + + _LOGGER.info("Connected to WiLight device: %s", api_device.device_id) + + await connect(api_device) + + self._api = api_device + + return True + + async def async_reset(self): + """Reset api.""" + + # If the initialization was wrong. + if self._api is None: + return True + + self._api.client.stop() + + +def create_api_device(host): + """Create an API Device.""" + try: + device = pywilight.device_from_host(host) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,) as err: + _LOGGER.error("Unable to access WiLight at %s (%s)", host, err) + return None + + return device diff --git a/homeassistant/components/wilight/strings.json b/homeassistant/components/wilight/strings.json new file mode 100644 index 00000000000..710543a5a53 --- /dev/null +++ b/homeassistant/components/wilight/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "title": "WiLight", + "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "not_supported_device": "This WiLight is currently not supported", + "not_wilight_device": "This Device is not WiLight" + } + } +} diff --git a/homeassistant/components/wilight/translations/en.json b/homeassistant/components/wilight/translations/en.json new file mode 100644 index 00000000000..fb121e3b2fa --- /dev/null +++ b/homeassistant/components/wilight/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured_device": "This WiLight is already configured", + "not_supported_device": "This WiLight is currently not supported", + "not_wilight_device": "This Device is not WiLight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "title": "WiLight", + "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b5db34ec485..ac9ebae5264 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -199,6 +199,7 @@ FLOWS = [ "volumio", "wemo", "wiffi", + "wilight", "withings", "wled", "wolflink", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index d58842fe88e..f66c5f0999d 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -172,5 +172,10 @@ SSDP = { { "manufacturer": "Belkin International Inc." } + ], + "wilight": [ + { + "manufacturer": "All Automacao Ltda" + } ] } diff --git a/requirements_all.txt b/requirements_all.txt index 153cb8117ad..3b60480f7da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1852,6 +1852,9 @@ pywebpush==1.9.2 # homeassistant.components.wemo pywemo==0.4.46 +# homeassistant.components.wilight +pywilight==0.0.65 + # homeassistant.components.xeoma pyxeoma==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3f45827bc4..f2c11ca739a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -857,6 +857,9 @@ pyvolumio==0.1.1 # homeassistant.components.html5 pywebpush==1.9.2 +# homeassistant.components.wilight +pywilight==0.0.65 + # homeassistant.components.zerproc pyzerproc==0.2.5 diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py new file mode 100644 index 00000000000..9c7ba13fc7c --- /dev/null +++ b/tests/components/wilight/__init__.py @@ -0,0 +1,83 @@ +"""Tests for the WiLight component.""" +from homeassistant.components.ssdp import ( + ATTR_SSDP_LOCATION, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_MODEL_NUMBER, + ATTR_UPNP_SERIAL, +) +from homeassistant.components.wilight.config_flow import ( + CONF_MODEL_NAME, + CONF_SERIAL_NUMBER, +) +from homeassistant.components.wilight.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +HOST = "127.0.0.1" +WILIGHT_ID = "000000000099" +SSDP_LOCATION = "http://127.0.0.1/" +UPNP_MANUFACTURER = "All Automacao Ltda" +UPNP_MODEL_NAME_P_B = "WiLight 0102001800010009-10010010" +UPNP_MODEL_NAME_DIMMER = "WiLight 0100001700020009-10010010" +UPNP_MODEL_NAME_COLOR = "WiLight 0107001800020009-11010" +UPNP_MODEL_NAME_LIGHT_FAN = "WiLight 0104001800010009-10" +UPNP_MODEL_NUMBER = "123456789012345678901234567890123456" +UPNP_SERIAL = "000000000099" +UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56" +UPNP_MANUFACTURER_NOT_WILIGHT = "Test" +CONF_COMPONENTS = "components" + +MOCK_SSDP_DISCOVERY_INFO_P_B = { + ATTR_SSDP_LOCATION: SSDP_LOCATION, + ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, + ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, + ATTR_UPNP_SERIAL: UPNP_SERIAL, +} + +MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER = { + ATTR_SSDP_LOCATION: SSDP_LOCATION, + ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER_NOT_WILIGHT, + ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, + ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, + ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, +} + +MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER = { + ATTR_SSDP_LOCATION: SSDP_LOCATION, + ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_P_B, + ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, + ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, +} + +MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN = { + ATTR_SSDP_LOCATION: SSDP_LOCATION, + ATTR_UPNP_MANUFACTURER: UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME: UPNP_MODEL_NAME_LIGHT_FAN, + ATTR_UPNP_MODEL_NUMBER: UPNP_MODEL_NUMBER, + ATTR_UPNP_SERIAL: ATTR_UPNP_SERIAL, +} + + +async def setup_integration(hass: HomeAssistantType,) -> MockConfigEntry: + """Mock ConfigEntry in Home Assistant.""" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=WILIGHT_ID, + data={ + CONF_HOST: HOST, + CONF_SERIAL_NUMBER: UPNP_SERIAL, + CONF_MODEL_NAME: UPNP_MODEL_NAME_P_B, + }, + ) + + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py new file mode 100644 index 00000000000..1de190c51d9 --- /dev/null +++ b/tests/components/wilight/test_config_flow.py @@ -0,0 +1,157 @@ +"""Test the WiLight config flow.""" +from asynctest import patch +import pytest + +from homeassistant.components.wilight.config_flow import ( + CONF_MODEL_NAME, + CONF_SERIAL_NUMBER, +) +from homeassistant.components.wilight.const import DOMAIN +from homeassistant.config_entries import SOURCE_SSDP +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry +from tests.components.wilight import ( + CONF_COMPONENTS, + HOST, + MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN, + MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER, + MOCK_SSDP_DISCOVERY_INFO_P_B, + MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER, + UPNP_MODEL_NAME_P_B, + UPNP_SERIAL, + WILIGHT_ID, +) + + +@pytest.fixture(name="dummy_get_components_from_model_clear") +def mock_dummy_get_components_from_model(): + """Mock a clear components list.""" + components = [] + with patch( + "pywilight.get_components_from_model", return_value=components, + ): + yield components + + +async def test_show_ssdp_form(hass: HomeAssistantType) -> None: + """Test that the ssdp confirmation form is served.""" + + discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["description_placeholders"] == { + CONF_NAME: f"WL{WILIGHT_ID}", + CONF_COMPONENTS: "light", + } + + +async def test_ssdp_not_wilight_abort_1(hass: HomeAssistantType) -> None: + """Test that the ssdp aborts not_wilight.""" + + discovery_info = MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "not_wilight_device" + + +async def test_ssdp_not_wilight_abort_2(hass: HomeAssistantType) -> None: + """Test that the ssdp aborts not_wilight.""" + + discovery_info = MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "not_wilight_device" + + +async def test_ssdp_not_wilight_abort_3( + hass: HomeAssistantType, dummy_get_components_from_model_clear +) -> None: + """Test that the ssdp aborts not_wilight.""" + + discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "not_wilight_device" + + +async def test_ssdp_not_supported_abort(hass: HomeAssistantType) -> None: + """Test that the ssdp aborts not_supported.""" + + discovery_info = MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "not_supported_device" + + +async def test_ssdp_device_exists_abort(hass: HomeAssistantType) -> None: + """Test abort SSDP flow if WiLight already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=WILIGHT_ID, + data={ + CONF_HOST: HOST, + CONF_SERIAL_NUMBER: UPNP_SERIAL, + CONF_MODEL_NAME: UPNP_MODEL_NAME_P_B, + }, + ) + + entry.add_to_hass(hass) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_full_ssdp_flow_implementation(hass: HomeAssistantType) -> None: + """Test the full SSDP flow from start to finish.""" + + discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["description_placeholders"] == { + CONF_NAME: f"WL{WILIGHT_ID}", + "components": "light", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"WL{WILIGHT_ID}" + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_SERIAL_NUMBER] == UPNP_SERIAL + assert result["data"][CONF_MODEL_NAME] == UPNP_MODEL_NAME_P_B diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py new file mode 100644 index 00000000000..11b86d0c367 --- /dev/null +++ b/tests/components/wilight/test_init.py @@ -0,0 +1,65 @@ +"""Tests for the WiLight integration.""" +from asynctest import patch +import pytest +import pywilight + +from homeassistant.components.wilight.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.components.wilight import ( + HOST, + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_P_B, + UPNP_MODEL_NUMBER, + UPNP_SERIAL, + setup_integration, +) + + +@pytest.fixture(name="dummy_device_from_host") +def mock_dummy_device_from_host(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_P_B, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", return_value=device, + ): + yield device + + +async def test_config_entry_not_ready(hass: HomeAssistantType) -> None: + """Test the WiLight configuration entry not ready.""" + entry = await setup_integration(hass) + + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry( + hass: HomeAssistantType, dummy_device_from_host +) -> None: + """Test the WiLight configuration entry unloading.""" + entry = await setup_integration(hass) + + assert entry.entry_id in hass.data[DOMAIN] + assert entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + if DOMAIN in hass.data: + assert entry.entry_id not in hass.data[DOMAIN] + assert entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py new file mode 100644 index 00000000000..555c1487bec --- /dev/null +++ b/tests/components/wilight/test_light.py @@ -0,0 +1,369 @@ +"""Tests for the WiLight integration.""" +from asynctest import patch +import pytest +import pywilight + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.components.wilight import ( + HOST, + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_COLOR, + UPNP_MODEL_NAME_DIMMER, + UPNP_MODEL_NAME_LIGHT_FAN, + UPNP_MODEL_NAME_P_B, + UPNP_MODEL_NUMBER, + UPNP_SERIAL, + WILIGHT_ID, + setup_integration, +) + + +@pytest.fixture(name="dummy_get_components_from_model_light") +def mock_dummy_get_components_from_model_light(): + """Mock a components list with light.""" + components = ["light"] + with patch( + "pywilight.get_components_from_model", return_value=components, + ): + yield components + + +@pytest.fixture(name="dummy_device_from_host_light_fan") +def mock_dummy_device_from_host_light_fan(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_LIGHT_FAN, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", return_value=device, + ): + yield device + + +@pytest.fixture(name="dummy_device_from_host_pb") +def mock_dummy_device_from_host_pb(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_P_B, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", return_value=device, + ): + yield device + + +@pytest.fixture(name="dummy_device_from_host_dimmer") +def mock_dummy_device_from_host_dimmer(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_DIMMER, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", return_value=device, + ): + yield device + + +@pytest.fixture(name="dummy_device_from_host_color") +def mock_dummy_device_from_host_color(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_COLOR, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", return_value=device, + ): + yield device + + +async def test_loading_light( + hass: HomeAssistantType, + dummy_device_from_host_light_fan, + dummy_get_components_from_model_light, +) -> None: + """Test the WiLight configuration entry loading.""" + + # Using light_fan and removind fan from get_components_from_model + # to test light.py line 28 + entry = await setup_integration(hass) + assert entry + assert entry.unique_id == WILIGHT_ID + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # First segment of the strip + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_OFF + + entry = entity_registry.async_get("light.wl000000000099_1") + assert entry + assert entry.unique_id == "WL000000000099_0" + + +async def test_on_off_light_state( + hass: HomeAssistantType, dummy_device_from_host_pb +) -> None: + """Test the change of state of the light switches.""" + await setup_integration(hass) + + # Turn on + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_ON + + # Turn off + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_OFF + + +async def test_dimmer_light_state( + hass: HomeAssistantType, dummy_device_from_host_dimmer +) -> None: + """Test the change of state of the light switches.""" + await setup_integration(hass) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_BRIGHTNESS: 42, ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_BRIGHTNESS) == 42 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_BRIGHTNESS: 0, ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_BRIGHTNESS: 100, ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_BRIGHTNESS) == 100 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_OFF + + # Turn on + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_ON + + +async def test_color_light_state( + hass: HomeAssistantType, dummy_device_from_host_color +) -> None: + """Test the change of state of the light switches.""" + await setup_integration(hass) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 42, + ATTR_HS_COLOR: [0, 100], + ATTR_ENTITY_ID: "light.wl000000000099_1", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_BRIGHTNESS) == 42 + state_color = [ + round(state.attributes.get(ATTR_HS_COLOR)[0]), + round(state.attributes.get(ATTR_HS_COLOR)[1]), + ] + assert state_color == [0, 100] + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_BRIGHTNESS: 0, ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 100, + ATTR_HS_COLOR: [270, 50], + ATTR_ENTITY_ID: "light.wl000000000099_1", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_BRIGHTNESS) == 100 + state_color = [ + round(state.attributes.get(ATTR_HS_COLOR)[0]), + round(state.attributes.get(ATTR_HS_COLOR)[1]), + ] + assert state_color == [270, 50] + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_OFF + + # Turn on + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_ON + + # Hue = 0, Saturation = 100 + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_HS_COLOR: [0, 100], ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_ON + state_color = [ + round(state.attributes.get(ATTR_HS_COLOR)[0]), + round(state.attributes.get(ATTR_HS_COLOR)[1]), + ] + assert state_color == [0, 100] + + # Brightness = 60 + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_BRIGHTNESS: 60, ATTR_ENTITY_ID: "light.wl000000000099_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wl000000000099_1") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_BRIGHTNESS) == 60 From 181709f3d29842886a3c84c02d4e374b57432437 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 24 Aug 2020 16:21:48 +0200 Subject: [PATCH 320/862] Allow templates in data & service parameters (making data_template & service_template obsolete) (#39210) --- homeassistant/helpers/config_validation.py | 35 ++++++++++++++++++---- homeassistant/helpers/script.py | 13 ++++---- homeassistant/helpers/service.py | 20 ++++++++----- tests/helpers/test_config_validation.py | 23 ++++++++++++++ tests/helpers/test_script.py | 12 +++++++- tests/helpers/test_service.py | 21 ++++++++++--- 6 files changed, 99 insertions(+), 25 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7ab81385c63..46b309815ab 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -429,6 +429,7 @@ def service(value: Any) -> str: str_value = string(value).lower() if valid_entity_id(str_value): return str_value + raise vol.Invalid(f"Service {value} does not match format .") @@ -527,6 +528,24 @@ def template(value: Optional[Any]) -> template_helper.Template: raise vol.Invalid(f"invalid template ({ex})") +def dynamic_template(value: Optional[Any]) -> template_helper.Template: + """Validate a dynamic (non static) jinja2 template.""" + + if value is None: + raise vol.Invalid("template value is None") + if isinstance(value, (list, dict, template_helper.Template)): + raise vol.Invalid("template value should be a string") + if not template_helper.is_template_string(str(value)): + raise vol.Invalid("template value does not contain a dynmamic template") + + template_value = template_helper.Template(str(value)) # type: ignore + try: + template_value.ensure_valid() + return cast(template_helper.Template, template_value) + except TemplateError as ex: + raise vol.Invalid(f"invalid template ({ex})") + + def template_complex(value: Any) -> Any: """Validate a complex jinja2 template.""" if isinstance(value, list): @@ -858,8 +877,8 @@ EVENT_SCHEMA = vol.Schema( { vol.Optional(CONF_ALIAS): string, vol.Required(CONF_EVENT): string, - vol.Optional(CONF_EVENT_DATA): dict, - vol.Optional(CONF_EVENT_DATA_TEMPLATE): template_complex, + vol.Optional(CONF_EVENT_DATA): vol.All(dict, template_complex), + vol.Optional(CONF_EVENT_DATA_TEMPLATE): vol.All(dict, template_complex), } ) @@ -867,10 +886,14 @@ SERVICE_SCHEMA = vol.All( vol.Schema( { vol.Optional(CONF_ALIAS): string, - vol.Exclusive(CONF_SERVICE, "service name"): service, - vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): template, - vol.Optional("data"): dict, - vol.Optional("data_template"): template_complex, + vol.Exclusive(CONF_SERVICE, "service name"): vol.Any( + service, dynamic_template + ), + vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): vol.Any( + service, dynamic_template + ), + vol.Optional("data"): vol.All(dict, template_complex), + vol.Optional("data_template"): vol.All(dict, template_complex), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, } ), diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d468d8a8dcf..c59d53f2e87 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -439,17 +439,18 @@ class _ScriptRun: CONF_ALIAS, self._action[CONF_EVENT] ) self._log("Executing step %s", self._script.last_action) - event_data = dict(self._action.get(CONF_EVENT_DATA, {})) - if CONF_EVENT_DATA_TEMPLATE in self._action: + event_data = {} + for conf in [CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE]: + if conf not in self._action: + continue + try: event_data.update( - template.render_complex( - self._action[CONF_EVENT_DATA_TEMPLATE], self._variables - ) + template.render_complex(self._action[conf], self._variables) ) except exceptions.TemplateError as ex: self._log( - "Error rendering event data template: %s", ex, level=logging.ERROR + "Error rendering event data template: %s", ex, level=logging.ERROR, ) self._hass.bus.async_fire( diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 082cd303e10..ad5a36467cf 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -35,6 +35,7 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, HomeAssistantType, TemplateVarsType from homeassistant.loader import async_get_integration, bind_hass from homeassistant.util.yaml import load_yaml @@ -110,9 +111,12 @@ def async_prepare_call_from_config( if CONF_SERVICE in config: domain_service = config[CONF_SERVICE] else: + domain_service = config[CONF_SERVICE_TEMPLATE] + + if isinstance(domain_service, Template): try: - config[CONF_SERVICE_TEMPLATE].hass = hass - domain_service = config[CONF_SERVICE_TEMPLATE].async_render(variables) + domain_service.hass = hass + domain_service = domain_service.async_render(variables) domain_service = cv.service(domain_service) except TemplateError as ex: raise HomeAssistantError( @@ -124,14 +128,14 @@ def async_prepare_call_from_config( ) from ex domain, service = domain_service.split(".", 1) - service_data = dict(config.get(CONF_SERVICE_DATA, {})) - if CONF_SERVICE_DATA_TEMPLATE in config: + service_data = {} + for conf in [CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE]: + if conf not in config: + continue try: - template.attach(hass, config[CONF_SERVICE_DATA_TEMPLATE]) - service_data.update( - template.render_complex(config[CONF_SERVICE_DATA_TEMPLATE], variables) - ) + template.attach(hass, config[conf]) + service_data.update(template.render_complex(config[conf], variables)) except TemplateError as ex: raise HomeAssistantError(f"Error rendering data template: {ex}") from ex diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 5898457d363..f7d7acbb08e 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -443,6 +443,29 @@ def test_template(): schema(value) +def test_dynamic_template(): + """Test dynamic template validator.""" + schema = vol.Schema(cv.dynamic_template) + + for value in ( + None, + 1, + "{{ partial_print }", + "{% if True %}Hello", + ["test"], + "just a string", + ): + with pytest.raises(vol.Invalid): + schema(value) + + options = ( + "{{ beer }}", + "{% if 1 == 1 %}Hello{% else %}World{% endif %}", + ) + for value in options: + schema(value) + + def test_template_complex(): """Test template_complex validator.""" schema = vol.Schema(cv.template_complex) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index d5aa15ffe38..ffbb8bb1cd7 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -71,7 +71,7 @@ async def test_firing_event_template(hass): sequence = cv.SCRIPT_SCHEMA( { "event": event, - "event_data_template": { + "event_data": { "dict": { 1: "{{ is_world }}", 2: "{{ is_world }}{{ is_world }}", @@ -79,6 +79,14 @@ async def test_firing_event_template(hass): }, "list": ["{{ is_world }}", "{{ is_world }}{{ is_world }}"], }, + "event_data_template": { + "dict2": { + 1: "{{ is_world }}", + 2: "{{ is_world }}{{ is_world }}", + 3: "{{ is_world }}{{ is_world }}{{ is_world }}", + }, + "list2": ["{{ is_world }}", "{{ is_world }}{{ is_world }}"], + }, } ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") @@ -91,6 +99,8 @@ async def test_firing_event_template(hass): assert events[0].data == { "dict": {1: "yes", 2: "yesyes", 3: "yesyesyes"}, "list": ["yes", "yesyes"], + "dict2": {1: "yes", 2: "yesyes", 3: "yesyesyes"}, + "list2": ["yes", "yesyes"], } diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index ba72cbc83ca..e3736aadceb 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -144,16 +144,16 @@ class TestServiceHelpers(unittest.TestCase): """Stop down everything that was started.""" self.hass.stop() - def test_template_service_call(self): + def test_service_call(self): """Test service call with templating.""" config = { - "service_template": "{{ 'test_domain.test_service' }}", + "service": "{{ 'test_domain.test_service' }}", "entity_id": "hello.world", - "data_template": { + "data": { "hello": "{{ 'goodbye' }}", "data": {"value": "{{ 'complex' }}", "simple": "simple"}, - "list": ["{{ 'list' }}", "2"], }, + "data_template": {"list": ["{{ 'list' }}", "2"]}, } service.call_from_config(self.hass, config) @@ -164,6 +164,19 @@ class TestServiceHelpers(unittest.TestCase): assert self.calls[0].data["data"]["simple"] == "simple" assert self.calls[0].data["list"][0] == "list" + def test_service_template_service_call(self): + """Test legacy service_template call with templating.""" + config = { + "service_template": "{{ 'test_domain.test_service' }}", + "entity_id": "hello.world", + "data": {"hello": "goodbye"}, + } + + service.call_from_config(self.hass, config) + self.hass.block_till_done() + + assert self.calls[0].data["hello"] == "goodbye" + def test_passing_variables_to_templates(self): """Test passing variables to templates.""" config = { From 28332f23b3a00e1fa3a695d54bb96ae4022f1abd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 24 Aug 2020 16:58:27 +0200 Subject: [PATCH 321/862] Don't sort keys when dumping json and yaml (#39214) --- homeassistant/components/http/view.py | 4 +--- homeassistant/util/json.py | 2 +- homeassistant/util/yaml/dumper.py | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 7c8e9281e42..376465f375b 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -47,9 +47,7 @@ class HomeAssistantView: ) -> web.Response: """Return a JSON response.""" try: - msg = json.dumps( - result, sort_keys=True, cls=JSONEncoder, allow_nan=False - ).encode("UTF-8") + msg = json.dumps(result, cls=JSONEncoder, allow_nan=False).encode("UTF-8") except (ValueError, TypeError) as err: _LOGGER.error("Unable to serialize to JSON: %s\n%s", err, result) raise HTTPInternalServerError diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 51d7c26a554..7b6da837c49 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -54,7 +54,7 @@ def save_json( Returns True on success. """ try: - json_data = json.dumps(data, sort_keys=True, indent=4, cls=encoder) + json_data = json.dumps(data, indent=4, cls=encoder) except TypeError: msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data))}" _LOGGER.error(msg) diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index 37df6bb89f5..3d11e943a91 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -10,9 +10,9 @@ from .objects import NodeListClass def dump(_dict: dict) -> str: """Dump YAML to a string and remove null.""" - return yaml.safe_dump(_dict, default_flow_style=False, allow_unicode=True).replace( - ": null\n", ":\n" - ) + return yaml.safe_dump( + _dict, default_flow_style=False, allow_unicode=True, sort_keys=False + ).replace(": null\n", ":\n") def save_yaml(path: str, data: dict) -> None: From bee6d87e7af337c8b98d4cc862ca9ca006d7f27e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Aug 2020 10:21:30 -0500 Subject: [PATCH 322/862] Standardize uuid generation for events/storage/registry (#39184) --- homeassistant/config_entries.py | 4 ++-- homeassistant/core.py | 11 ++--------- homeassistant/helpers/area_registry.py | 4 ++-- homeassistant/helpers/device_registry.py | 4 ++-- homeassistant/util/uuid.py | 15 +++++++++++++++ tests/util/test_uuid.py | 11 +++++++++++ 6 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 homeassistant/util/uuid.py create mode 100644 tests/util/test_uuid.py diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9bfc9a1f1d0..04bdbf236a5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -4,7 +4,6 @@ import functools import logging from types import MappingProxyType from typing import Any, Callable, Dict, List, Optional, Set, Union, cast -import uuid import weakref import attr @@ -16,6 +15,7 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.event import Event from homeassistant.setup import async_process_deps_reqs, async_setup_component from homeassistant.util.decorator import Registry +import homeassistant.util.uuid as uuid_util _LOGGER = logging.getLogger(__name__) _UNDEF: dict = {} @@ -135,7 +135,7 @@ class ConfigEntry: ) -> None: """Initialize a config entry.""" # Unique id of the config entry - self.entry_id = entry_id or uuid.uuid4().hex + self.entry_id = entry_id or uuid_util.uuid_v1mc_hex() # Version of the configuration. self.version = version diff --git a/homeassistant/core.py b/homeassistant/core.py index 7b812096fcb..31e132eec1a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -12,7 +12,6 @@ from ipaddress import ip_address import logging import os import pathlib -import random import re import threading from time import monotonic @@ -33,7 +32,6 @@ from typing import ( Union, cast, ) -import uuid import attr import voluptuous as vol @@ -77,6 +75,7 @@ import homeassistant.util.dt as dt_util from homeassistant.util.thread import fix_threading_exception_logging from homeassistant.util.timeout import TimeoutManager from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem +import homeassistant.util.uuid as uuid_util # Typing imports that create a circular dependency if TYPE_CHECKING: @@ -510,13 +509,7 @@ class Context: user_id: str = attr.ib(default=None) parent_id: Optional[str] = attr.ib(default=None) - # The uuid1 uses a random multicast MAC address instead of the real MAC address - # of the machine without the overhead of calling the getrandom() system call. - # - # This is effectively equivalent to PostgreSQL's uuid_generate_v1mc() function - id: str = attr.ib( - factory=lambda: uuid.uuid1(node=random.getrandbits(48) | (1 << 40)).hex - ) + id: str = attr.ib(factory=uuid_util.uuid_v1mc_hex) def as_dict(self) -> dict: """Return a dictionary representation of the context.""" diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 5def290766f..3e3a3e03f6a 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -3,12 +3,12 @@ from asyncio import Event from collections import OrderedDict import logging from typing import Dict, Iterable, List, MutableMapping, Optional, cast -import uuid import attr from homeassistant.core import callback from homeassistant.loader import bind_hass +import homeassistant.util.uuid as uuid_util from .typing import HomeAssistantType @@ -26,7 +26,7 @@ class AreaEntry: """Area Registry Entry.""" name: Optional[str] = attr.ib(default=None) - id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + id: str = attr.ib(factory=uuid_util.uuid_v1mc_hex) class AreaRegistry: diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a52a3837868..7c990aa397d 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,12 +2,12 @@ from collections import OrderedDict import logging from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union -import uuid import attr from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, callback +import homeassistant.util.uuid as uuid_util from .debounce import Debouncer from .singleton import singleton @@ -73,7 +73,7 @@ class DeviceEntry: area_id: str = attr.ib(default=None) name_by_user: str = attr.ib(default=None) entry_type: str = attr.ib(default=None) - id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + id: str = attr.ib(factory=uuid_util.uuid_v1mc_hex) # This value is not stored, just used to keep track of events to fire. is_new: bool = attr.ib(default=False) diff --git a/homeassistant/util/uuid.py b/homeassistant/util/uuid.py new file mode 100644 index 00000000000..c91cfe0dd12 --- /dev/null +++ b/homeassistant/util/uuid.py @@ -0,0 +1,15 @@ +"""Helpers to generate uuids.""" + +import random +import uuid + + +def uuid_v1mc_hex() -> str: + """Generate a uuid1 with a random multicast MAC address. + + The uuid1 uses a random multicast MAC address instead of the real MAC address + of the machine without the overhead of calling the getrandom() system call. + + This is effectively equivalent to PostgreSQL's uuid_generate_v1mc() function + """ + return uuid.uuid1(node=random.getrandbits(48) | (1 << 40)).hex diff --git a/tests/util/test_uuid.py b/tests/util/test_uuid.py new file mode 100644 index 00000000000..0debb867341 --- /dev/null +++ b/tests/util/test_uuid.py @@ -0,0 +1,11 @@ +"""Test Home Assistant uuid util methods.""" + +import uuid + +import homeassistant.util.uuid as uuid_util + + +async def test_uuid_v1mc_hex(): + """Verify we can generate a uuid_v1mc and return hex.""" + assert len(uuid_util.uuid_v1mc_hex()) == 32 + assert uuid.UUID(uuid_util.uuid_v1mc_hex()) From b1c0d8fb6c12450afc3ff9fc5d73d91097251a06 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 24 Aug 2020 17:28:07 +0200 Subject: [PATCH 323/862] Minor cleanup of MQTT ACK handling (#39217) --- homeassistant/components/mqtt/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 439f37473a5..819fbc9838f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -938,7 +938,10 @@ class MQTT: """Publish / Subscribe / Unsubscribe callback.""" self.hass.add_job(self._mqtt_handle_mid, mid) - async def _mqtt_handle_mid(self, mid) -> None: + @callback + def _mqtt_handle_mid(self, mid) -> None: + # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid + # may be executed first. if mid not in self._pending_operations: self._pending_operations[mid] = asyncio.Event() self._pending_operations[mid].set() @@ -956,6 +959,8 @@ class MQTT: async def _wait_for_mid(self, mid): """Wait for ACK from broker.""" + # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid + # may be executed first. if mid not in self._pending_operations: self._pending_operations[mid] = asyncio.Event() try: From 6b7a7939d21e7073f5a33d179169f5b5fd4c08da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Aug 2020 12:44:40 -0500 Subject: [PATCH 324/862] Include the first seen context data in the logbook api (#39194) * Include the context_entity_id in the logbook api context_entity_id is the first entity seen during a time period that includes the context * update test * more of them * include friendly name * pylint wants a ternary * Refactor * performance * fix homekit context * Fix self describing events * Fix external_events --- .../components/homekit/accessories.py | 9 +- homeassistant/components/logbook/__init__.py | 179 +++++++-- .../components/template/template_entity.py | 3 + homeassistant/scripts/benchmark/__init__.py | 2 +- tests/components/alexa/test_init.py | 1 + tests/components/automation/test_init.py | 1 + tests/components/homekit/test_init.py | 1 + tests/components/logbook/test_init.py | 377 +++++++++++++++--- tests/components/script/test_init.py | 1 + 9 files changed, 473 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index b2fe3ca3f54..68b61772ce8 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -31,7 +31,7 @@ from homeassistant.const import ( UNIT_PERCENTAGE, __version__, ) -from homeassistant.core import callback as ha_callback, split_entity_id +from homeassistant.core import Context, callback as ha_callback, split_entity_id from homeassistant.helpers.event import ( async_track_state_change_event, track_point_in_utc_time, @@ -490,9 +490,12 @@ class HomeAccessory(Accessory): ATTR_SERVICE: service, ATTR_VALUE: value, } + context = Context() - self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data) - await self.hass.services.async_call(domain, service, service_data) + self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data, context=context) + await self.hass.services.async_call( + domain, service, service_data, context=context + ) @ha_callback def async_stop(self): diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 28f85cf92da..fb219de0d8c 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import aliased import voluptuous as vol from homeassistant.components import sun +from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.history import sqlalchemy_filter_from_include_exclude_conf from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder.models import ( @@ -18,12 +19,15 @@ from homeassistant.components.recorder.models import ( process_timestamp_to_utc_isoformat, ) from homeassistant.components.recorder.util import session_scope +from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME, + ATTR_SERVICE, + EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, @@ -70,7 +74,15 @@ HOMEASSISTANT_EVENTS = [ EVENT_HOMEASSISTANT_STOP, ] -ALL_EVENT_TYPES = [EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, *HOMEASSISTANT_EVENTS] +ALL_EVENT_TYPES = [ + EVENT_STATE_CHANGED, + EVENT_LOGBOOK_ENTRY, + EVENT_CALL_SERVICE, + *HOMEASSISTANT_EVENTS, +] + +SCRIPT_AUTOMATION_EVENTS = [EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED] + LOG_MESSAGE_SCHEMA = vol.Schema( { @@ -83,13 +95,13 @@ LOG_MESSAGE_SCHEMA = vol.Schema( @bind_hass -def log_entry(hass, name, message, domain=None, entity_id=None): +def log_entry(hass, name, message, domain=None, entity_id=None, context=None): """Add an entry to the logbook.""" - hass.add_job(async_log_entry, hass, name, message, domain, entity_id) + hass.add_job(async_log_entry, hass, name, message, domain, entity_id, context) @bind_hass -def async_log_entry(hass, name, message, domain=None, entity_id=None): +def async_log_entry(hass, name, message, domain=None, entity_id=None, context=None): """Add an entry to the logbook.""" data = {ATTR_NAME: name, ATTR_MESSAGE: message} @@ -97,7 +109,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None): data[ATTR_DOMAIN] = domain if entity_id is not None: data[ATTR_ENTITY_ID] = entity_id - hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) + hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data, context=context) async def async_setup(hass, config): @@ -203,7 +215,6 @@ class LogbookView(HomeAssistantView): return self.json( _get_events( hass, - self.config, start_day, end_day, entity_id, @@ -215,7 +226,7 @@ class LogbookView(HomeAssistantView): return await hass.async_add_executor_job(json_events) -def humanify(hass, events, entity_attr_cache): +def humanify(hass, events, entity_attr_cache, context_lookup): """Generate a converted list of events into Entry objects. Will try to group events if possible: @@ -263,7 +274,18 @@ def humanify(hass, events, entity_attr_cache): data = describe_event(event) data["when"] = event.time_fired_isoformat data["domain"] = domain - data["context_user_id"] = event.context_user_id + if event.context_user_id: + data["context_user_id"] = event.context_user_id + context_event = context_lookup.get(event.context_id) + if context_event: + _augment_data_with_context( + data, + data.get(ATTR_ENTITY_ID), + event, + context_event, + entity_attr_cache, + external_events, + ) yield data if event.event_type == EVENT_STATE_CHANGED: @@ -277,21 +299,34 @@ def humanify(hass, events, entity_attr_cache): # Skip all but the last sensor state continue - name = entity_attr_cache.get( - entity_id, ATTR_FRIENDLY_NAME, event - ) or split_entity_id(entity_id)[1].replace("_", " ") - - yield { + data = { "when": event.time_fired_isoformat, - "name": name, + "name": _entity_name_from_event( + entity_id, event, entity_attr_cache + ), "message": _entry_message_from_event( - hass, entity_id, domain, event, entity_attr_cache + entity_id, domain, event, entity_attr_cache ), "domain": domain, "entity_id": entity_id, - "context_user_id": event.context_user_id, } + if event.context_user_id: + data["context_user_id"] = event.context_user_id + + context_event = context_lookup.get(event.context_id) + if context_event and context_event != event: + _augment_data_with_context( + data, + entity_id, + event, + context_event, + entity_attr_cache, + external_events, + ) + + yield data + elif event.event_type == EVENT_HOMEASSISTANT_START: if start_stop_events.get(event.time_fired_minute) == 2: continue @@ -301,7 +336,6 @@ def humanify(hass, events, entity_attr_cache): "name": "Home Assistant", "message": "started", "domain": HA_DOMAIN, - "context_user_id": event.context_user_id, } elif event.event_type == EVENT_HOMEASSISTANT_STOP: @@ -315,7 +349,6 @@ def humanify(hass, events, entity_attr_cache): "name": "Home Assistant", "message": action, "domain": HA_DOMAIN, - "context_user_id": event.context_user_id, } elif event.event_type == EVENT_LOGBOOK_ENTRY: @@ -328,25 +361,42 @@ def humanify(hass, events, entity_attr_cache): except IndexError: pass - yield { + data = { "when": event.time_fired_isoformat, "name": event_data.get(ATTR_NAME), "message": event_data.get(ATTR_MESSAGE), "domain": domain, "entity_id": entity_id, } + if event.context_user_id: + data["context_user_id"] = event.context_user_id + + context_event = context_lookup.get(event.context_id) + if context_event and context_event != event: + _augment_data_with_context( + data, + entity_id, + event, + context_event, + entity_attr_cache, + external_events, + ) + + yield data def _get_events( - hass, config, start_day, end_day, entity_id=None, filters=None, entities_filter=None + hass, start_day, end_day, entity_id=None, filters=None, entities_filter=None ): """Get events for a period of time.""" entity_attr_cache = EntityAttributeCache(hass) + context_lookup = {None: None} def yield_events(query): """Yield Events that are not filtered away.""" for row in query.yield_per(1000): event = LazyEventPartialState(row) + context_lookup.setdefault(event.context_id, event) if _keep_event(hass, event, entities_filter): yield event @@ -366,6 +416,7 @@ def _get_events( Events.event_type, Events.event_data, Events.time_fired, + Events.context_id, Events.context_user_id, States.state, States.entity_id, @@ -424,7 +475,9 @@ def _get_events( entity_filter | (Events.event_type != EVENT_STATE_CHANGED) ) - return list(humanify(hass, yield_events(query), entity_attr_cache)) + return list( + humanify(hass, yield_events(query), entity_attr_cache, context_lookup) + ) def _keep_event(hass, event, entities_filter): @@ -439,10 +492,12 @@ def _keep_event(hass, event, entities_filter): if domain is None: return False entity_id = f"{domain}." + elif event.event_type == EVENT_CALL_SERVICE: + return False else: event_data = event.data entity_id = event_data.get(ATTR_ENTITY_ID) - if entity_id is None: + if not entity_id: domain = event_data.get(ATTR_DOMAIN) if domain is None: return False @@ -451,7 +506,7 @@ def _keep_event(hass, event, entities_filter): return entities_filter is None or entities_filter(entity_id) -def _entry_message_from_event(hass, entity_id, domain, event, entity_attr_cache): +def _entry_message_from_event(entity_id, domain, event, entity_attr_cache): """Convert a state to a message for the logbook.""" # We pass domain in so we don't have to split entity_id again state_state = event.state @@ -539,6 +594,68 @@ def _entry_message_from_event(hass, entity_id, domain, event, entity_attr_cache) return f"changed to {state_state}" +def _augment_data_with_context( + data, entity_id, event, context_event, entity_attr_cache, external_events +): + event_type = context_event.event_type + + # State change + context_entity_id = context_event.entity_id + + if entity_id and context_entity_id == entity_id: + return + + if context_entity_id: + data["context_entity_id"] = context_entity_id + data["context_entity_id_name"] = _entity_name_from_event( + context_entity_id, context_event, entity_attr_cache + ) + data["context_event_type"] = event_type + return + + event_data = context_event.data + + # Call service + if event_type == EVENT_CALL_SERVICE: + event_data = context_event.data + data["context_domain"] = event_data.get(ATTR_DOMAIN) + data["context_service"] = event_data.get(ATTR_SERVICE) + data["context_event_type"] = event_type + return + + if not entity_id: + return + + attr_entity_id = event_data.get(ATTR_ENTITY_ID) + if not attr_entity_id or ( + event_type in SCRIPT_AUTOMATION_EVENTS and attr_entity_id == entity_id + ): + return + + if context_event == event: + return + + data["context_entity_id"] = attr_entity_id + data["context_entity_id_name"] = _entity_name_from_event( + attr_entity_id, context_event, entity_attr_cache + ) + data["context_event_type"] = event_type + + if event_type in external_events: + domain, describe_event = external_events[event_type] + data["context_domain"] = domain + name = describe_event(context_event).get(ATTR_NAME) + if name: + data["context_name"] = name + + +def _entity_name_from_event(entity_id, event, entity_attr_cache): + """Extract the entity name from the event using the cache if possible.""" + return entity_attr_cache.get( + entity_id, ATTR_FRIENDLY_NAME, event + ) or split_entity_id(entity_id)[1].replace("_", " ") + + class LazyEventPartialState: """A lazy version of core Event with limited State joined in.""" @@ -552,6 +669,9 @@ class LazyEventPartialState: "entity_id", "state", "domain", + "context_id", + "context_user_id", + "time_fired_minute", ] def __init__(self, row): @@ -565,11 +685,9 @@ class LazyEventPartialState: self.entity_id = self._row.entity_id self.state = self._row.state self.domain = self._row.domain - - @property - def context_user_id(self): - """Context user id of event.""" - return self._row.context_user_id + self.context_id = self._row.context_id + self.context_user_id = self._row.context_user_id + self.time_fired_minute = self._row.time_fired.minute @property def attributes(self): @@ -594,11 +712,6 @@ class LazyEventPartialState: self._event_data = json.loads(self._row.event_data) return self._event_data - @property - def time_fired_minute(self): - """Minute the event was fired not converted.""" - return self._row.time_fired.minute - @property def time_fired(self): """Time event was fired in utc.""" diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index a65f5cdd29f..d63a2866510 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -66,6 +66,9 @@ class _TemplateAttribute: last_result: Optional[str], result: Union[str, TemplateError], ) -> None: + if event: + self._entity.async_set_context(event.context) + if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') " diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index d2091bb6003..48e6d7d5302 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -231,7 +231,7 @@ async def _logbook_filtering(hass, last_changed, last_updated): start = timer() - list(logbook.humanify(hass, yield_events(event), entity_attr_cache)) + list(logbook.humanify(hass, yield_events(event), entity_attr_cache, {})) return timer() - start diff --git a/tests/components/alexa/test_init.py b/tests/components/alexa/test_init.py index 605ca96f190..c0972351cce 100644 --- a/tests/components/alexa/test_init.py +++ b/tests/components/alexa/test_init.py @@ -44,6 +44,7 @@ async def test_humanify_alexa_event(hass): ), ], entity_attr_cache, + {}, ) ) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 483f8003e3f..7179fbdaae2 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1089,6 +1089,7 @@ async def test_logbook_humanify_automation_triggered_event(hass): ), ], entity_attr_cache, + {}, ) ) diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 1fad563445b..72670667fa7 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -44,6 +44,7 @@ async def test_humanify_homekit_changed_event(hass, hk_driver): ), ], entity_attr_cache, + {}, ) ) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index f264f75e2b0..78753e2a67e 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -15,12 +15,16 @@ from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, ATTR_NAME, + ATTR_SERVICE, CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, @@ -96,7 +100,6 @@ class TestComponentLogbook(unittest.TestCase): events = list( logbook._get_events( self.hass, - {}, dt_util.utcnow() - timedelta(hours=1), dt_util.utcnow() + timedelta(hours=1), ) @@ -152,7 +155,7 @@ class TestComponentLogbook(unittest.TestCase): eventC = self.create_state_changed_event(pointC, entity_id, 30) entries = list( - logbook.humanify(self.hass, (eventA, eventB, eventC), entity_attr_cache) + logbook.humanify(self.hass, (eventA, eventB, eventC), entity_attr_cache, {}) ) assert len(entries) == 2 @@ -191,7 +194,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 2 self.assert_entry( @@ -229,7 +232,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 2 self.assert_entry( @@ -276,7 +279,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 2 self.assert_entry( @@ -318,7 +321,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 2 self.assert_entry( @@ -364,7 +367,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 3 self.assert_entry( @@ -418,7 +421,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 4 self.assert_entry( @@ -475,7 +478,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 5 self.assert_entry( @@ -549,7 +552,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 6 self.assert_entry( @@ -585,6 +588,7 @@ class TestComponentLogbook(unittest.TestCase): MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), ), entity_attr_cache, + {}, ), ) @@ -607,6 +611,7 @@ class TestComponentLogbook(unittest.TestCase): self.create_state_changed_event(pointA, entity_id, 10), ), entity_attr_cache, + {}, ) ) @@ -628,21 +633,21 @@ class TestComponentLogbook(unittest.TestCase): # message for a device state change eventA = self.create_state_changed_event(pointA, "switch.bla", 10) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "changed to 10" # message for a switch turned on eventA = self.create_state_changed_event(pointA, "switch.bla", STATE_ON) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "turned on" # message for a switch turned off eventA = self.create_state_changed_event(pointA, "switch.bla", STATE_OFF) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "turned off" @@ -656,14 +661,14 @@ class TestComponentLogbook(unittest.TestCase): pointA, "device_tracker.john", STATE_NOT_HOME ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is away" # message for a device tracker "home" state eventA = self.create_state_changed_event(pointA, "device_tracker.john", "work") message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is at work" @@ -675,14 +680,14 @@ class TestComponentLogbook(unittest.TestCase): # message for a device tracker "not home" state eventA = self.create_state_changed_event(pointA, "person.john", STATE_NOT_HOME) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is away" # message for a device tracker "home" state eventA = self.create_state_changed_event(pointA, "person.john", "work") message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is at work" @@ -696,7 +701,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "sun.sun", sun.STATE_ABOVE_HORIZON ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "has risen" @@ -705,7 +710,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "sun.sun", sun.STATE_BELOW_HORIZON ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "has set" @@ -720,7 +725,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.battery", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is low" @@ -729,7 +734,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.battery", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is normal" @@ -744,7 +749,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.connectivity", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is connected" @@ -753,7 +758,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.connectivity", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is disconnected" @@ -768,7 +773,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.door", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is opened" @@ -777,7 +782,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.door", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is closed" @@ -792,7 +797,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.garage_door", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is opened" @@ -801,7 +806,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.garage_door", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is closed" @@ -816,7 +821,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.opening", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is opened" @@ -825,7 +830,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.opening", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is closed" @@ -840,7 +845,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.window", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is opened" @@ -849,7 +854,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.window", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is closed" @@ -864,7 +869,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.lock", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is unlocked" @@ -873,7 +878,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.lock", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is locked" @@ -888,7 +893,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.plug", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is plugged in" @@ -897,7 +902,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.plug", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is unplugged" @@ -912,7 +917,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.presence", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is at home" @@ -921,7 +926,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.presence", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is away" @@ -936,7 +941,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.safety", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is unsafe" @@ -945,7 +950,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.safety", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is safe" @@ -960,7 +965,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.cold", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected cold" @@ -969,7 +974,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.cold", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no cold detected)" @@ -984,7 +989,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.gas", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected gas" @@ -993,7 +998,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.gas", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no gas detected)" @@ -1008,7 +1013,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.heat", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected heat" @@ -1017,7 +1022,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.heat", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no heat detected)" @@ -1032,7 +1037,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.light", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected light" @@ -1041,7 +1046,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.light", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no light detected)" @@ -1056,7 +1061,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.moisture", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected moisture" @@ -1065,7 +1070,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.moisture", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no moisture detected)" @@ -1080,7 +1085,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.motion", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected motion" @@ -1089,7 +1094,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.motion", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no motion detected)" @@ -1104,7 +1109,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.occupancy", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected occupancy" @@ -1113,7 +1118,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.occupancy", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no occupancy detected)" @@ -1128,7 +1133,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.power", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected power" @@ -1137,7 +1142,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.power", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no power detected)" @@ -1152,7 +1157,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.problem", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected problem" @@ -1161,7 +1166,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.problem", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no problem detected)" @@ -1176,7 +1181,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.smoke", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected smoke" @@ -1185,7 +1190,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.smoke", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no smoke detected)" @@ -1200,7 +1205,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.sound", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected sound" @@ -1209,7 +1214,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.sound", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no sound detected)" @@ -1224,7 +1229,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.vibration", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected vibration" @@ -1233,7 +1238,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.vibration", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no vibration detected)" @@ -1258,6 +1263,7 @@ class TestComponentLogbook(unittest.TestCase): ), ), entity_attr_cache, + {}, ) ) @@ -1652,7 +1658,6 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client): assert response.status == 200 json_dict = await response.json() - assert len(json_dict) == 5 assert json_dict[0]["entity_id"] == entity_id_test assert json_dict[1]["entity_id"] == entity_id_second assert json_dict[2]["entity_id"] == "automation.mock_automation" @@ -1837,6 +1842,245 @@ async def test_exclude_attribute_changes(hass, hass_client): assert response_json[2]["entity_id"] == "light.kitchen" +async def test_logbook_entity_context_id(hass, hass_client): + """Test the logbook view with end_time and entity with automations and scripts.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", {}) + await async_setup_component(hass, "automation", {}) + await async_setup_component(hass, "script", {}) + + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + context = ha.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + + # An Automation + automation_entity_id_test = "automation.alarm" + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: automation_entity_id_test}, + context=context, + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + context=context, + ) + hass.states.async_set( + automation_entity_id_test, + STATE_ON, + {ATTR_FRIENDLY_NAME: "Alarm Automation"}, + context=context, + ) + + entity_id_test = "alarm_control_panel.area_001" + hass.states.async_set(entity_id_test, STATE_OFF, context=context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_test, STATE_ON, context=context) + await hass.async_block_till_done() + entity_id_second = "alarm_control_panel.area_002" + hass.states.async_set(entity_id_second, STATE_OFF, context=context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_second, STATE_ON, context=context) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + await hass.async_add_job( + logbook.log_entry, + hass, + "mock_name", + "mock_message", + "alarm_control_panel", + "alarm_control_panel.area_003", + context, + ) + await hass.async_block_till_done() + + await hass.async_add_job( + logbook.log_entry, + hass, + "mock_name", + "mock_message", + "homeassistant", + None, + context, + ) + await hass.async_block_till_done() + + # A service call + light_turn_off_service_context = ha.Context( + id="9c5bd62de45711eaaeb351041eec8dd9", + user_id="9400facee45711eaa9308bfd3d19e474", + ) + hass.states.async_set("light.switch", STATE_ON) + await hass.async_block_till_done() + + hass.bus.async_fire( + EVENT_CALL_SERVICE, + { + ATTR_DOMAIN: "light", + ATTR_SERVICE: "turn_off", + ATTR_ENTITY_ID: "light.switch", + }, + context=light_turn_off_service_context, + ) + await hass.async_block_till_done() + + hass.states.async_set( + "light.switch", STATE_OFF, context=light_turn_off_service_context + ) + await hass.async_block_till_done() + + await hass.async_add_job(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 with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) + assert response.status == 200 + json_dict = await response.json() + + assert json_dict[0]["entity_id"] == "automation.alarm" + assert "context_entity_id" not in json_dict[0] + assert json_dict[0]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[1]["entity_id"] == "script.mock_script" + assert json_dict[1]["context_event_type"] == "automation_triggered" + assert json_dict[1]["context_entity_id"] == "automation.alarm" + assert json_dict[1]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[1]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[2]["entity_id"] == entity_id_test + assert json_dict[2]["context_event_type"] == "automation_triggered" + assert json_dict[2]["context_entity_id"] == "automation.alarm" + assert json_dict[2]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[2]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[3]["entity_id"] == entity_id_second + assert json_dict[3]["context_event_type"] == "automation_triggered" + assert json_dict[3]["context_entity_id"] == "automation.alarm" + assert json_dict[3]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[4]["domain"] == "homeassistant" + + assert json_dict[5]["entity_id"] == "alarm_control_panel.area_003" + assert json_dict[5]["context_event_type"] == "automation_triggered" + assert json_dict[5]["context_entity_id"] == "automation.alarm" + assert json_dict[5]["domain"] == "alarm_control_panel" + assert json_dict[5]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[5]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[6]["domain"] == "homeassistant" + assert json_dict[6]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[7]["entity_id"] == "light.switch" + assert json_dict[7]["context_event_type"] == "call_service" + assert json_dict[7]["context_domain"] == "light" + assert json_dict[7]["context_service"] == "turn_off" + assert json_dict[7]["domain"] == "light" + assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + +async def test_logbook_context_from_template(hass, hass_client): + """Test the logbook view with end_time and entity with automations and scripts.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", {}) + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ states.switch.test_state.state }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + } + }, + } + }, + ) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + # Entity added (should not be logged) + hass.states.async_set("switch.test_state", STATE_ON) + await hass.async_block_till_done() + + # First state change (should be logged) + hass.states.async_set("switch.test_state", STATE_OFF) + await hass.async_block_till_done() + + switch_turn_off_context = ha.Context( + id="9c5bd62de45711eaaeb351041eec8dd9", + user_id="9400facee45711eaa9308bfd3d19e474", + ) + hass.states.async_set( + "switch.test_state", STATE_ON, context=switch_turn_off_context + ) + await hass.async_block_till_done() + + await hass.async_add_job(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 with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) + assert response.status == 200 + json_dict = await response.json() + + assert json_dict[0]["domain"] == "homeassistant" + assert "context_entity_id" not in json_dict[0] + + assert json_dict[1]["entity_id"] == "switch.test_template_switch" + + assert json_dict[2]["entity_id"] == "switch.test_state" + + assert json_dict[3]["entity_id"] == "switch.test_template_switch" + assert json_dict[3]["context_entity_id"] == "switch.test_state" + assert json_dict[3]["context_entity_id_name"] == "test state" + + assert json_dict[4]["entity_id"] == "switch.test_state" + assert json_dict[4]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + assert json_dict[5]["entity_id"] == "switch.test_template_switch" + assert json_dict[5]["context_entity_id"] == "switch.test_state" + assert json_dict[5]["context_entity_id_name"] == "test state" + assert json_dict[5]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + class MockLazyEventPartialState(ha.Event): """Minimal mock of a Lazy event.""" @@ -1850,6 +2094,11 @@ class MockLazyEventPartialState(ha.Event): """Context user id of event.""" return self.context.user_id + @property + def context_id(self): + """Context id of event.""" + return self.context.id + @property def time_fired_isoformat(self): """Time event was fired in utc isoformat.""" diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 584c44916c7..22625d46530 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -516,6 +516,7 @@ async def test_logbook_humanify_script_started_event(hass): ), ], entity_attr_cache, + {}, ) ) From d5193e64de6ce6a4460003d5f47351a92f35bba3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 24 Aug 2020 21:33:58 +0200 Subject: [PATCH 325/862] Updated frontend to 20200824.0 (#39224) --- 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 e55cbb2bc83..976f78afc4a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200820.0"], + "requirements": ["home-assistant-frontend==20200824.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b01126b9574..c6a543e94d7 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.35.0 -home-assistant-frontend==20200820.0 +home-assistant-frontend==20200824.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3b60480f7da..dea11aaab98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -745,7 +745,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200820.0 +home-assistant-frontend==20200824.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2c11ca739a..758b78191e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -371,7 +371,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200820.0 +home-assistant-frontend==20200824.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 2a1fe9d29a8764c612f89a4e808d2db6b0603e12 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 24 Aug 2020 23:01:57 +0200 Subject: [PATCH 326/862] Add websocket trigger/condition commands (#39109) --- .../components/automation/__init__.py | 2 +- .../homeassistant/triggers/event.py | 7 +- .../homeassistant/triggers/homeassistant.py | 7 +- .../homeassistant/triggers/numeric_state.py | 27 +++---- .../homeassistant/triggers/state.py | 23 +++--- homeassistant/components/template/trigger.py | 23 +++--- .../components/websocket_api/commands.py | 68 ++++++++++++++++ homeassistant/components/zone/trigger.py | 25 +++--- homeassistant/core.py | 3 + .../components/websocket_api/test_commands.py | 80 ++++++++++++++++++- 10 files changed, 204 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c550aa4f2c7..970653cd4df 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -377,7 +377,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): else: await self.async_disable() - async def async_trigger(self, variables, skip_condition=False, context=None): + async def async_trigger(self, variables, context=None, skip_condition=False): """Trigger automation. This method is a coroutine. diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 9fc78746a7c..2c247280e06 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -47,10 +47,9 @@ async def async_attach_trigger( return hass.async_run_job( - action( - {"trigger": {"platform": platform_type, "event": event}}, - context=event.context, - ) + action, + {"trigger": {"platform": platform_type, "event": event}}, + event.context, ) return hass.bus.async_listen(event_type, handle_event) diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 91b67e28c7c..6ccb54d034e 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -30,10 +30,9 @@ async def async_attach_trigger(hass, config, action, automation_info): def hass_shutdown(event): """Execute when Home Assistant is shutting down.""" hass.async_run_job( - action( - {"trigger": {"platform": "homeassistant", "event": event}}, - context=event.context, - ) + action, + {"trigger": {"platform": "homeassistant", "event": event}}, + event.context, ) return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 1429fa7ce7b..b2958f4d63a 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -103,20 +103,19 @@ async def async_attach_trigger( def call_action(): """Call action with right context.""" hass.async_run_job( - action( - { - "trigger": { - "platform": platform_type, - "entity_id": entity, - "below": below, - "above": above, - "from_state": from_s, - "to_state": to_s, - "for": time_delta if not time_delta else period[entity], - } - }, - context=to_s.context, - ) + action, + { + "trigger": { + "platform": platform_type, + "entity_id": entity, + "below": below, + "above": above, + "from_state": from_s, + "to_state": to_s, + "for": time_delta if not time_delta else period[entity], + } + }, + to_s.context, ) matching = check_numeric_state(entity, from_s, to_s) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 603fff5993e..9ddd1b3fcb8 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -83,18 +83,17 @@ async def async_attach_trigger( def call_action(): """Call action with right context.""" hass.async_run_job( - action( - { - "trigger": { - "platform": platform_type, - "entity_id": entity, - "from_state": from_s, - "to_state": to_s, - "for": time_delta if not time_delta else period[entity], - } - }, - context=event.context, - ) + action, + { + "trigger": { + "platform": platform_type, + "entity_id": entity, + "from_state": from_s, + "to_state": to_s, + "for": time_delta if not time_delta else period[entity], + } + }, + event.context, ) if not time_delta: diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 032ffa813f5..b6e6c974807 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -54,18 +54,17 @@ async def async_attach_trigger( def call_action(*_): """Call action with right context.""" hass.async_run_job( - action( - { - "trigger": { - "platform": "template", - "entity_id": entity_id, - "from_state": from_s, - "to_state": to_s, - "for": time_delta if not time_delta else period, - } - }, - context=(to_s.context if to_s else None), - ) + action, + { + "trigger": { + "platform": "template", + "entity_id": entity_id, + "from_state": from_s, + "to_state": to_s, + "for": time_delta if not time_delta else period, + } + }, + (to_s.context if to_s else None), ) if not time_delta: diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b44ca45ce03..4ed0292a9f4 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -40,6 +40,8 @@ def async_register_commands(hass, async_reg): async_reg(hass, handle_manifest_list) async_reg(hass, handle_manifest_get) async_reg(hass, handle_entity_source) + async_reg(hass, handle_subscribe_trigger) + async_reg(hass, handle_test_condition) def pong_message(iden): @@ -315,3 +317,69 @@ def handle_entity_source(hass, connection, msg): sources[entity_id] = source connection.send_result(msg["id"], sources) + + +@callback +@decorators.websocket_command( + { + vol.Required("type"): "subscribe_trigger", + vol.Required("trigger"): cv.TRIGGER_SCHEMA, + vol.Optional("variables"): dict, + } +) +@decorators.require_admin +@decorators.async_response +async def handle_subscribe_trigger(hass, connection, msg): + """Handle subscribe trigger command.""" + # Circular dep + # pylint: disable=import-outside-toplevel + from homeassistant.helpers import trigger + + trigger_config = await trigger.async_validate_trigger_config(hass, msg["trigger"]) + + @callback + def forward_triggers(variables, context=None): + """Forward events to websocket.""" + connection.send_message( + messages.event_message( + msg["id"], {"variables": variables, "context": context} + ) + ) + + connection.subscriptions[msg["id"]] = ( + await trigger.async_initialize_triggers( + hass, + trigger_config, + forward_triggers, + const.DOMAIN, + const.DOMAIN, + connection.logger.log, + variables=msg.get("variables"), + ) + ) or ( + # Some triggers won't return an unsub function. Since the caller expects + # a subscription, we're going to fake one. + lambda: None + ) + connection.send_result(msg["id"]) + + +@decorators.websocket_command( + { + vol.Required("type"): "test_condition", + vol.Required("condition"): cv.CONDITION_SCHEMA, + vol.Optional("variables"): dict, + } +) +@decorators.require_admin +@decorators.async_response +async def handle_test_condition(hass, connection, msg): + """Handle test condition command.""" + # Circular dep + # pylint: disable=import-outside-toplevel + from homeassistant.helpers import condition + + check_condition = await condition.async_from_config(hass, msg["condition"]) + connection.send_result( + msg["id"], {"result": check_condition(hass, msg.get("variables"))} + ) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 3b794f698a1..f53af0e5d7f 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -57,19 +57,18 @@ async def async_attach_trigger(hass, config, action, automation_info): and not to_match ): hass.async_run_job( - action( - { - "trigger": { - "platform": "zone", - "entity_id": entity, - "from_state": from_s, - "to_state": to_s, - "zone": zone_state, - "event": event, - } - }, - context=to_s.context, - ) + action, + { + "trigger": { + "platform": "zone", + "entity_id": entity, + "from_state": from_s, + "to_state": to_s, + "zone": zone_state, + "event": event, + } + }, + to_s.context, ) return async_track_state_change_event(hass, entity_id, zone_automation_listener) diff --git a/homeassistant/core.py b/homeassistant/core.py index 31e132eec1a..c5a54f0374f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -302,6 +302,9 @@ class HomeAssistant: target: target to call. args: parameters for method to call. """ + if target is None: + raise ValueError("Don't call async_add_job with None") + task = None # Check for partials to properly determine if coroutine function diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 5e35f2de04f..4113a833872 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -8,7 +8,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.const import URL -from homeassistant.core import callback +from homeassistant.core import Context, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity from homeassistant.loader import async_get_integration @@ -654,3 +654,81 @@ async def test_entity_source_admin(hass, websocket_client, hass_admin_user): assert msg["type"] == const.TYPE_RESULT assert not msg["success"] assert msg["error"]["code"] == const.ERR_UNAUTHORIZED + + +async def test_subscribe_trigger(hass, websocket_client): + """Test subscribing to a trigger.""" + init_count = sum(hass.bus.async_listeners().values()) + + await websocket_client.send_json( + { + "id": 5, + "type": "subscribe_trigger", + "trigger": {"platform": "event", "event_type": "test_event"}, + "variables": {"hello": "world"}, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + # Verify we have a new listener + assert sum(hass.bus.async_listeners().values()) == init_count + 1 + + context = Context() + + hass.bus.async_fire("ignore_event") + hass.bus.async_fire("test_event", {"hello": "world"}, context=context) + hass.bus.async_fire("ignore_event") + + with timeout(3): + msg = await websocket_client.receive_json() + + assert msg["id"] == 5 + assert msg["type"] == "event" + assert msg["event"]["context"]["id"] == context.id + assert msg["event"]["variables"]["trigger"]["platform"] == "event" + + event = msg["event"]["variables"]["trigger"]["event"] + + assert event["event_type"] == "test_event" + assert event["data"] == {"hello": "world"} + assert event["origin"] == "LOCAL" + + await websocket_client.send_json( + {"id": 6, "type": "unsubscribe_events", "subscription": 5} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +async def test_test_condition(hass, websocket_client): + """Test testing a condition.""" + hass.states.async_set("hello.world", "paulus") + + await websocket_client.send_json( + { + "id": 5, + "type": "test_condition", + "condition": { + "condition": "state", + "entity_id": "hello.world", + "state": "paulus", + }, + "variables": {"hello": "world"}, + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"]["result"] is True From 3a7620bd08af7aa8019680f35414ed605f4c323c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Aug 2020 21:35:48 -0500 Subject: [PATCH 327/862] Update sense icon mappings (#39225) --- homeassistant/components/sense/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index 07dacc288b5..783fcb5508a 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -55,7 +55,7 @@ MDI_ICONS = { "papershredder": "shredder", "printer": "printer", "pump": "water-pump", - "settings": "settings", + "settings": "cog", "skillet": "pot", "smartcamera": "webcam", "socket": "power-plug", From 7d4a9ac65adb5a31bf254d3117c3ed174ae4f10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ak=C4=B1n=20=C3=96mero=C4=9Flu?= Date: Tue, 25 Aug 2020 10:32:48 +0300 Subject: [PATCH 328/862] Bump python-temascal to 0.2 for lg_soundbar (#39213) --- homeassistant/components/lg_soundbar/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index 42b5e22570c..8ec7c789166 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -2,6 +2,6 @@ "domain": "lg_soundbar", "name": "LG Soundbars", "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", - "requirements": ["temescal==0.1"], + "requirements": ["temescal==0.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index dea11aaab98..3785cff85df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2117,7 +2117,7 @@ tellcore-py==1.1.2 tellduslive==0.10.11 # homeassistant.components.lg_soundbar -temescal==0.1 +temescal==0.2 # homeassistant.components.temper temperusb==1.5.3 From ef35eea0f6d788b7cf6e5647c9f8573198cfa607 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Tue, 25 Aug 2020 13:49:00 +0200 Subject: [PATCH 329/862] Remove 'entity_id' from ToggleRflinkLight (#37992) Remove `entity_id` overwrite from `ToggleRflinkLight` class --- homeassistant/components/rflink/light.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 65d6acb3de9..13e3a0f65da 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -280,11 +280,6 @@ class ToggleRflinkLight(SwitchableRflinkDevice, LightEntity): and if the light is off and 'on' gets sent, the light will turn on. """ - @property - def entity_id(self): - """Return entity id.""" - return f"light.{self.name}" - def _handle_event(self, event): """Adjust state if Rflink picks up a remote command for this device.""" self.cancel_queued_send_commands() From 13df3bce1b9c7ce3cd871540fdafe156ad979271 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Aug 2020 13:49:32 +0200 Subject: [PATCH 330/862] Allow owner users to change password of any user (#39242) --- homeassistant/auth/providers/homeassistant.py | 38 ++++ .../config/auth_provider_homeassistant.py | 127 ++++++----- .../test_auth_provider_homeassistant.py | 213 ++++++++++++------ 3 files changed, 251 insertions(+), 127 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 65f738b3412..cd10cf7cf95 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -30,6 +30,15 @@ def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]: CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id) +async def async_get_provider(hass: HomeAssistant) -> "HassAuthProvider": + """Get the provider.""" + for prv in hass.auth.auth_providers: + if prv.type == "homeassistant": + return cast(HassAuthProvider, prv) + + raise RuntimeError("Provider not found") + + class InvalidAuth(HomeAssistantError): """Raised when we encounter invalid authentication.""" @@ -235,6 +244,35 @@ class HassAuthProvider(AuthProvider): self.data.validate_login, username, password ) + async def async_add_auth(self, username: str, password: str) -> None: + """Call add_auth on data.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + await self.hass.async_add_executor_job(self.data.add_auth, username, password) + await self.data.async_save() + + async def async_remove_auth(self, username: str) -> None: + """Call remove_auth on data.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + self.data.async_remove_auth(username) + await self.data.async_save() + + async def async_change_password(self, username: str, new_password: str) -> None: + """Call change_password on data.""" + if self.data is None: + await self.async_initialize() + assert self.data is not None + + await self.hass.async_add_executor_job( + self.data.change_password, username, new_password + ) + await self.data.async_save() + async def async_get_or_create_credentials( self, flow_result: Dict[str, str] ) -> Credentials: diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index dec7fb24d27..1eb410e3c98 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -3,62 +3,34 @@ import voluptuous as vol from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components import websocket_api +from homeassistant.components.websocket_api import decorators +from homeassistant.exceptions import Unauthorized -WS_TYPE_CREATE = "config/auth_provider/homeassistant/create" -SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + +async def async_setup(hass): + """Enable the Home Assistant views.""" + hass.components.websocket_api.async_register_command(websocket_create) + hass.components.websocket_api.async_register_command(websocket_delete) + hass.components.websocket_api.async_register_command(websocket_change_password) + hass.components.websocket_api.async_register_command( + websocket_admin_change_password + ) + return True + + +@decorators.websocket_command( { - vol.Required("type"): WS_TYPE_CREATE, + vol.Required("type"): "config/auth_provider/homeassistant/create", vol.Required("user_id"): str, vol.Required("username"): str, vol.Required("password"): str, } ) - -WS_TYPE_DELETE = "config/auth_provider/homeassistant/delete" -SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_DELETE, vol.Required("username"): str} -) - -WS_TYPE_CHANGE_PASSWORD = "config/auth_provider/homeassistant/change_password" -SCHEMA_WS_CHANGE_PASSWORD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_CHANGE_PASSWORD, - vol.Required("current_password"): str, - vol.Required("new_password"): str, - } -) - - -async def async_setup(hass): - """Enable the Home Assistant views.""" - hass.components.websocket_api.async_register_command( - WS_TYPE_CREATE, websocket_create, SCHEMA_WS_CREATE - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_DELETE, websocket_delete, SCHEMA_WS_DELETE - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_CHANGE_PASSWORD, websocket_change_password, SCHEMA_WS_CHANGE_PASSWORD - ) - return True - - -def _get_provider(hass): - """Get homeassistant auth provider.""" - for prv in hass.auth.auth_providers: - if prv.type == "homeassistant": - return prv - - raise RuntimeError("Provider not found") - - @websocket_api.require_admin @websocket_api.async_response async def websocket_create(hass, connection, msg): """Create credentials and attach to a user.""" - provider = _get_provider(hass) - await provider.async_initialize() - + provider = await auth_ha.async_get_provider(hass) user = await hass.auth.async_get_user(msg["user_id"]) if user is None: @@ -78,9 +50,7 @@ async def websocket_create(hass, connection, msg): return try: - await hass.async_add_executor_job( - provider.data.add_auth, msg["username"], msg["password"] - ) + await provider.async_add_auth(msg["username"], msg["password"]) except auth_ha.InvalidUser: connection.send_message( websocket_api.error_message( @@ -94,17 +64,20 @@ async def websocket_create(hass, connection, msg): ) await hass.auth.async_link_user(user, credentials) - await provider.data.async_save() connection.send_message(websocket_api.result_message(msg["id"])) +@decorators.websocket_command( + { + vol.Required("type"): "config/auth_provider/homeassistant/delete", + vol.Required("username"): str, + } +) @websocket_api.require_admin @websocket_api.async_response async def websocket_delete(hass, connection, msg): """Delete username and related credential.""" - provider = _get_provider(hass) - await provider.async_initialize() - + provider = await auth_ha.async_get_provider(hass) credentials = await provider.async_get_or_create_credentials( {"username": msg["username"]} ) @@ -118,8 +91,7 @@ async def websocket_delete(hass, connection, msg): return try: - provider.data.async_remove_auth(msg["username"]) - await provider.data.async_save() + await provider.async_remove_auth(msg["username"]) except auth_ha.InvalidUser: connection.send_message( websocket_api.error_message( @@ -131,9 +103,16 @@ async def websocket_delete(hass, connection, msg): connection.send_message(websocket_api.result_message(msg["id"])) +@decorators.websocket_command( + { + vol.Required("type"): "config/auth_provider/homeassistant/change_password", + vol.Required("current_password"): str, + vol.Required("new_password"): str, + } +) @websocket_api.async_response async def websocket_change_password(hass, connection, msg): - """Change user password.""" + """Change current user password.""" user = connection.user if user is None: connection.send_message( @@ -141,9 +120,7 @@ async def websocket_change_password(hass, connection, msg): ) return - provider = _get_provider(hass) - await provider.async_initialize() - + provider = await auth_ha.async_get_provider(hass) username = None for credential in user.credentials: if credential.auth_provider_type == provider.type: @@ -168,9 +145,35 @@ async def websocket_change_password(hass, connection, msg): ) return - await hass.async_add_executor_job( - provider.data.change_password, username, msg["new_password"] - ) - await provider.data.async_save() + await provider.async_change_password(username, msg["new_password"]) connection.send_message(websocket_api.result_message(msg["id"])) + + +@decorators.websocket_command( + { + vol.Required( + "type" + ): "config/auth_provider/homeassistant/admin_change_password", + vol.Required("username"): str, + vol.Required("password"): str, + } +) +@decorators.require_admin +@decorators.async_response +async def websocket_admin_change_password(hass, connection, msg): + """Change password of any user.""" + if not connection.user.is_owner: + raise Unauthorized(context=connection.context(msg)) + + provider = await auth_ha.async_get_provider(hass) + try: + await provider.async_change_password(msg["username"], msg["password"]) + connection.send_message(websocket_api.result_message(msg["id"])) + except auth_ha.InvalidUser: + connection.send_message( + websocket_api.error_message( + msg["id"], "credentials_not_found", "Credentials not found" + ) + ) + return diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index 0a2f195e333..ab0682e8eaa 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -4,7 +4,7 @@ import pytest from homeassistant.auth.providers import homeassistant as prov_ha from homeassistant.components.config import auth_provider_homeassistant as auth_ha -from tests.common import MockUser, register_auth_provider +from tests.common import CLIENT_ID, MockUser, register_auth_provider @pytest.fixture(autouse=True) @@ -16,17 +16,44 @@ def setup_config(hass): hass.loop.run_until_complete(auth_ha.async_setup(hass)) -async def test_create_auth_system_generated_user( - hass, hass_access_token, hass_ws_client -): +@pytest.fixture +async def auth_provider(hass): + """Hass auth provider.""" + provider = hass.auth.auth_providers[0] + await provider.async_initialize() + return provider + + +@pytest.fixture +async def owner_access_token(hass, hass_owner_user): + """Access token for owner user.""" + refresh_token = await hass.auth.async_create_refresh_token( + hass_owner_user, CLIENT_ID + ) + return hass.auth.async_create_access_token(refresh_token) + + +@pytest.fixture +async def test_user_credential(hass, auth_provider): + """Add a test user.""" + await hass.async_add_executor_job( + auth_provider.data.add_auth, "test-user", "test-pass" + ) + + return await auth_provider.async_get_or_create_credentials( + {"username": "test-user"} + ) + + +async def test_create_auth_system_generated_user(hass, hass_ws_client): """Test we can't add auth to system generated users.""" system_user = MockUser(system_generated=True).add_to_hass(hass) - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) await client.send_json( { "id": 5, - "type": auth_ha.WS_TYPE_CREATE, + "type": "config/auth_provider/homeassistant/create", "user_id": system_user.id, "username": "test-user", "password": "test-pass", @@ -44,14 +71,14 @@ async def test_create_auth_user_already_credentials(): # assert False -async def test_create_auth_unknown_user(hass_ws_client, hass, hass_access_token): +async def test_create_auth_unknown_user(hass_ws_client, hass): """Test create pointing at unknown user.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) await client.send_json( { "id": 5, - "type": auth_ha.WS_TYPE_CREATE, + "type": "config/auth_provider/homeassistant/create", "user_id": "test-id", "username": "test-user", "password": "test-pass", @@ -73,7 +100,7 @@ async def test_create_auth_requires_admin( await client.send_json( { "id": 5, - "type": auth_ha.WS_TYPE_CREATE, + "type": "config/auth_provider/homeassistant/create", "user_id": "test-id", "username": "test-user", "password": "test-pass", @@ -85,9 +112,9 @@ async def test_create_auth_requires_admin( assert result["error"]["code"] == "unauthorized" -async def test_create_auth(hass, hass_ws_client, hass_access_token, hass_storage): +async def test_create_auth(hass, hass_ws_client, hass_storage): """Test create auth command works.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) user = MockUser().add_to_hass(hass) assert len(user.credentials) == 0 @@ -95,7 +122,7 @@ async def test_create_auth(hass, hass_ws_client, hass_access_token, hass_storage await client.send_json( { "id": 5, - "type": auth_ha.WS_TYPE_CREATE, + "type": "config/auth_provider/homeassistant/create", "user_id": user.id, "username": "test-user", "password": "test-pass", @@ -114,11 +141,9 @@ async def test_create_auth(hass, hass_ws_client, hass_access_token, hass_storage assert entry["username"] == "test-user" -async def test_create_auth_duplicate_username( - hass, hass_ws_client, hass_access_token, hass_storage -): +async def test_create_auth_duplicate_username(hass, hass_ws_client, hass_storage): """Test we can't create auth with a duplicate username.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) user = MockUser().add_to_hass(hass) hass_storage[prov_ha.STORAGE_KEY] = { @@ -129,7 +154,7 @@ async def test_create_auth_duplicate_username( await client.send_json( { "id": 5, - "type": auth_ha.WS_TYPE_CREATE, + "type": "config/auth_provider/homeassistant/create", "user_id": user.id, "username": "test-user", "password": "test-pass", @@ -141,11 +166,9 @@ async def test_create_auth_duplicate_username( assert result["error"]["code"] == "username_exists" -async def test_delete_removes_just_auth( - hass_ws_client, hass, hass_storage, hass_access_token -): +async def test_delete_removes_just_auth(hass_ws_client, hass, hass_storage): """Test deleting an auth without being connected to a user.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) hass_storage[prov_ha.STORAGE_KEY] = { "version": 1, @@ -153,7 +176,11 @@ async def test_delete_removes_just_auth( } await client.send_json( - {"id": 5, "type": auth_ha.WS_TYPE_DELETE, "username": "test-user"} + { + "id": 5, + "type": "config/auth_provider/homeassistant/delete", + "username": "test-user", + } ) result = await client.receive_json() @@ -161,11 +188,9 @@ async def test_delete_removes_just_auth( assert len(hass_storage[prov_ha.STORAGE_KEY]["data"]["users"]) == 0 -async def test_delete_removes_credential( - hass, hass_ws_client, hass_access_token, hass_storage -): +async def test_delete_removes_credential(hass, hass_ws_client, hass_storage): """Test deleting auth that is connected to a user.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) user = MockUser().add_to_hass(hass) hass_storage[prov_ha.STORAGE_KEY] = { @@ -180,7 +205,11 @@ async def test_delete_removes_credential( ) await client.send_json( - {"id": 5, "type": auth_ha.WS_TYPE_DELETE, "username": "test-user"} + { + "id": 5, + "type": "config/auth_provider/homeassistant/delete", + "username": "test-user", + } ) result = await client.receive_json() @@ -193,7 +222,11 @@ async def test_delete_requires_admin(hass, hass_ws_client, hass_read_only_access client = await hass_ws_client(hass, hass_read_only_access_token) await client.send_json( - {"id": 5, "type": auth_ha.WS_TYPE_DELETE, "username": "test-user"} + { + "id": 5, + "type": "config/auth_provider/homeassistant/delete", + "username": "test-user", + } ) result = await client.receive_json() @@ -201,12 +234,16 @@ async def test_delete_requires_admin(hass, hass_ws_client, hass_read_only_access assert result["error"]["code"] == "unauthorized" -async def test_delete_unknown_auth(hass, hass_ws_client, hass_access_token): +async def test_delete_unknown_auth(hass, hass_ws_client): """Test trying to delete an unknown auth username.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) await client.send_json( - {"id": 5, "type": auth_ha.WS_TYPE_DELETE, "username": "test-user"} + { + "id": 5, + "type": "config/auth_provider/homeassistant/delete", + "username": "test-user", + } ) result = await client.receive_json() @@ -214,25 +251,17 @@ async def test_delete_unknown_auth(hass, hass_ws_client, hass_access_token): assert result["error"]["code"] == "auth_not_found" -async def test_change_password(hass, hass_ws_client, hass_access_token): +async def test_change_password( + hass, hass_ws_client, hass_admin_user, auth_provider, test_user_credential +): """Test that change password succeeds with valid password.""" - provider = hass.auth.auth_providers[0] - await provider.async_initialize() - await hass.async_add_executor_job(provider.data.add_auth, "test-user", "test-pass") + await hass.auth.async_link_user(hass_admin_user, test_user_credential) - credentials = await provider.async_get_or_create_credentials( - {"username": "test-user"} - ) - - refresh_token = await hass.auth.async_validate_access_token(hass_access_token) - user = refresh_token.user - await hass.auth.async_link_user(user, credentials) - - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) await client.send_json( { "id": 6, - "type": auth_ha.WS_TYPE_CHANGE_PASSWORD, + "type": "config/auth_provider/homeassistant/change_password", "current_password": "test-pass", "new_password": "new-pass", } @@ -240,28 +269,20 @@ async def test_change_password(hass, hass_ws_client, hass_access_token): result = await client.receive_json() assert result["success"], result - await provider.async_validate_login("test-user", "new-pass") + await auth_provider.async_validate_login("test-user", "new-pass") -async def test_change_password_wrong_pw(hass, hass_ws_client, hass_access_token): +async def test_change_password_wrong_pw( + hass, hass_ws_client, hass_admin_user, auth_provider, test_user_credential +): """Test that change password fails with invalid password.""" - provider = hass.auth.auth_providers[0] - await provider.async_initialize() - await hass.async_add_executor_job(provider.data.add_auth, "test-user", "test-pass") + await hass.auth.async_link_user(hass_admin_user, test_user_credential) - credentials = await provider.async_get_or_create_credentials( - {"username": "test-user"} - ) - - refresh_token = await hass.auth.async_validate_access_token(hass_access_token) - user = refresh_token.user - await hass.auth.async_link_user(user, credentials) - - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) await client.send_json( { "id": 6, - "type": auth_ha.WS_TYPE_CHANGE_PASSWORD, + "type": "config/auth_provider/homeassistant/change_password", "current_password": "wrong-pass", "new_password": "new-pass", } @@ -271,17 +292,17 @@ async def test_change_password_wrong_pw(hass, hass_ws_client, hass_access_token) assert not result["success"], result assert result["error"]["code"] == "invalid_password" with pytest.raises(prov_ha.InvalidAuth): - await provider.async_validate_login("test-user", "new-pass") + await auth_provider.async_validate_login("test-user", "new-pass") -async def test_change_password_no_creds(hass, hass_ws_client, hass_access_token): +async def test_change_password_no_creds(hass, hass_ws_client): """Test that change password fails with no credentials.""" - client = await hass_ws_client(hass, hass_access_token) + client = await hass_ws_client(hass) await client.send_json( { "id": 6, - "type": auth_ha.WS_TYPE_CHANGE_PASSWORD, + "type": "config/auth_provider/homeassistant/change_password", "current_password": "test-pass", "new_password": "new-pass", } @@ -290,3 +311,65 @@ async def test_change_password_no_creds(hass, hass_ws_client, hass_access_token) result = await client.receive_json() assert not result["success"], result assert result["error"]["code"] == "credentials_not_found" + + +async def test_admin_change_password_not_owner( + hass, hass_ws_client, auth_provider, test_user_credential +): + """Test that change password fails when not owner.""" + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": "config/auth_provider/homeassistant/admin_change_password", + "username": "test-user", + "password": "new-pass", + } + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "unauthorized" + + # Validate old login still works + await auth_provider.async_validate_login("test-user", "test-pass") + + +async def test_admin_change_password_no_creds(hass, hass_ws_client, owner_access_token): + """Test that change password fails with unknown credentials.""" + client = await hass_ws_client(hass, owner_access_token) + + await client.send_json( + { + "id": 6, + "type": "config/auth_provider/homeassistant/admin_change_password", + "username": "non-existing", + "password": "new-pass", + } + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "credentials_not_found" + + +async def test_admin_change_password( + hass, hass_ws_client, owner_access_token, auth_provider, test_user_credential +): + """Test that owners can change any password.""" + client = await hass_ws_client(hass, owner_access_token) + + await client.send_json( + { + "id": 6, + "type": "config/auth_provider/homeassistant/admin_change_password", + "username": "test-user", + "password": "new-pass", + } + ) + + result = await client.receive_json() + assert result["success"], result + + await auth_provider.async_validate_login("test-user", "new-pass") From 9979e465aa39627f3710cc8365eb53994536f822 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Aug 2020 14:22:50 +0200 Subject: [PATCH 331/862] Fix hassio auth data (#39244) Co-authored-by: Pascal Vizeli --- homeassistant/auth/providers/homeassistant.py | 3 +- .../config/auth_provider_homeassistant.py | 8 +- homeassistant/components/hassio/auth.py | 88 ++++++++----------- tests/components/hassio/test_auth.py | 62 ++++++------- 4 files changed, 66 insertions(+), 95 deletions(-) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index cd10cf7cf95..70e2f5403cd 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -30,7 +30,8 @@ def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]: CONFIG_SCHEMA = vol.All(AUTH_PROVIDER_SCHEMA, _disallow_id) -async def async_get_provider(hass: HomeAssistant) -> "HassAuthProvider": +@callback +def async_get_provider(hass: HomeAssistant) -> "HassAuthProvider": """Get the provider.""" for prv in hass.auth.auth_providers: if prv.type == "homeassistant": diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 1eb410e3c98..696d215f68b 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -30,7 +30,7 @@ async def async_setup(hass): @websocket_api.async_response async def websocket_create(hass, connection, msg): """Create credentials and attach to a user.""" - provider = await auth_ha.async_get_provider(hass) + provider = auth_ha.async_get_provider(hass) user = await hass.auth.async_get_user(msg["user_id"]) if user is None: @@ -77,7 +77,7 @@ async def websocket_create(hass, connection, msg): @websocket_api.async_response async def websocket_delete(hass, connection, msg): """Delete username and related credential.""" - provider = await auth_ha.async_get_provider(hass) + provider = auth_ha.async_get_provider(hass) credentials = await provider.async_get_or_create_credentials( {"username": msg["username"]} ) @@ -120,7 +120,7 @@ async def websocket_change_password(hass, connection, msg): ) return - provider = await auth_ha.async_get_provider(hass) + provider = auth_ha.async_get_provider(hass) username = None for credential in user.credentials: if credential.auth_provider_type == provider.type: @@ -166,7 +166,7 @@ async def websocket_admin_change_password(hass, connection, msg): if not connection.user.is_owner: raise Unauthorized(context=connection.context(msg)) - provider = await auth_ha.async_get_provider(hass) + provider = auth_ha.async_get_provider(hass) try: await provider.async_change_password(msg["username"], msg["password"]) connection.send_message(websocket_api.result_message(msg["id"])) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 48f0abd6617..fb2b1dc757c 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -4,20 +4,16 @@ import logging import os from aiohttp import web -from aiohttp.web_exceptions import ( - HTTPInternalServerError, - HTTPNotFound, - HTTPUnauthorized, -) +from aiohttp.web_exceptions import HTTPNotFound, HTTPUnauthorized import voluptuous as vol from homeassistant.auth.models import User +from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import HTTP_OK from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType @@ -26,21 +22,6 @@ from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME _LOGGER = logging.getLogger(__name__) -SCHEMA_API_AUTH = vol.Schema( - { - vol.Required(ATTR_USERNAME): cv.string, - vol.Required(ATTR_PASSWORD): cv.string, - vol.Required(ATTR_ADDON): cv.string, - }, - extra=vol.ALLOW_EXTRA, -) - -SCHEMA_API_PASSWORD_RESET = vol.Schema( - {vol.Required(ATTR_USERNAME): cv.string, vol.Required(ATTR_PASSWORD): cv.string}, - extra=vol.ALLOW_EXTRA, -) - - @callback def async_setup_auth_view(hass: HomeAssistantType, user: User): """Auth setup.""" @@ -74,15 +55,6 @@ class HassIOBaseAuth(HomeAssistantView): _LOGGER.error("Invalid auth request from %s", request[KEY_HASS_USER].name) raise HTTPUnauthorized() - def _get_provider(self): - """Return Homeassistant auth provider.""" - prv = self.hass.auth.get_auth_provider("homeassistant", None) - if prv is not None: - return prv - - _LOGGER.error("Can't find Home Assistant auth") - raise HTTPNotFound() - class HassIOAuth(HassIOBaseAuth): """Hass.io view to handle auth requests.""" @@ -90,23 +62,30 @@ class HassIOAuth(HassIOBaseAuth): name = "api:hassio:auth" url = "/api/hassio_auth" - @RequestDataValidator(SCHEMA_API_AUTH) + @RequestDataValidator( + vol.Schema( + { + vol.Required(ATTR_USERNAME): cv.string, + vol.Required(ATTR_PASSWORD): cv.string, + vol.Required(ATTR_ADDON): cv.string, + }, + extra=vol.ALLOW_EXTRA, + ) + ) async def post(self, request, data): """Handle auth requests.""" self._check_access(request) - - await self._check_login(data[ATTR_USERNAME], data[ATTR_PASSWORD]) - return web.Response(status=HTTP_OK) - - async def _check_login(self, username, password): - """Check User credentials.""" - provider = self._get_provider() + provider = auth_ha.async_get_provider(request.app["hass"]) try: - await provider.async_validate_login(username, password) - except HomeAssistantError: + await provider.async_validate_login( + data[ATTR_USERNAME], data[ATTR_PASSWORD] + ) + except auth_ha.InvalidAuth: raise HTTPUnauthorized() from None + return web.Response(status=HTTP_OK) + class HassIOPasswordReset(HassIOBaseAuth): """Hass.io view to handle password reset requests.""" @@ -114,22 +93,25 @@ class HassIOPasswordReset(HassIOBaseAuth): name = "api:hassio:auth:password:reset" url = "/api/hassio_auth/password_reset" - @RequestDataValidator(SCHEMA_API_PASSWORD_RESET) + @RequestDataValidator( + vol.Schema( + { + vol.Required(ATTR_USERNAME): cv.string, + vol.Required(ATTR_PASSWORD): cv.string, + }, + extra=vol.ALLOW_EXTRA, + ) + ) async def post(self, request, data): """Handle password reset requests.""" self._check_access(request) - - await self._change_password(data[ATTR_USERNAME], data[ATTR_PASSWORD]) - return web.Response(status=HTTP_OK) - - async def _change_password(self, username, password): - """Check User credentials.""" - provider = self._get_provider() + provider = auth_ha.async_get_provider(request.app["hass"]) try: - await self.hass.async_add_executor_job( - provider.data.change_password, username, password + await provider.async_change_password( + data[ATTR_USERNAME], data[ATTR_PASSWORD] ) - await provider.data.async_save() - except HomeAssistantError: - raise HTTPInternalServerError() + except auth_ha.InvalidUser: + raise HTTPNotFound() + + return web.Response(status=HTTP_OK) diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index e97c5bc66fb..c5ac9df74b7 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,7 +1,6 @@ """The tests for the hassio component.""" -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR -from homeassistant.exceptions import HomeAssistantError +from homeassistant.auth.providers.homeassistant import InvalidAuth from tests.async_mock import Mock, patch @@ -59,7 +58,7 @@ async def test_login_error(hass, hassio_client_supervisor): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(side_effect=HomeAssistantError()), + Mock(side_effect=InvalidAuth()), ) as mock_login: resp = await hassio_client_supervisor.post( "/api/hassio_auth", @@ -76,7 +75,7 @@ async def test_login_no_data(hass, hassio_client_supervisor): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(side_effect=HomeAssistantError()), + Mock(side_effect=InvalidAuth()), ) as mock_login: resp = await hassio_client_supervisor.post("/api/hassio_auth") @@ -90,7 +89,7 @@ async def test_login_no_username(hass, hassio_client_supervisor): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(side_effect=HomeAssistantError()), + Mock(side_effect=InvalidAuth()), ) as mock_login: resp = await hassio_client_supervisor.post( "/api/hassio_auth", json={"password": "123456", "addon": "samba"} @@ -125,7 +124,8 @@ async def test_login_success_extra(hass, hassio_client_supervisor): async def test_password_success(hass, hassio_client_supervisor): """Test no auth needed for .""" with patch( - "homeassistant.components.hassio.auth.HassIOPasswordReset._change_password", + "homeassistant.auth.providers.homeassistant." + "HassAuthProvider.async_change_password", ) as mock_change: resp = await hassio_client_supervisor.post( "/api/hassio_auth/password_reset", @@ -139,44 +139,32 @@ async def test_password_success(hass, hassio_client_supervisor): async def test_password_fails_no_supervisor(hass, hassio_client): """Test if only supervisor can access.""" - with patch( - "homeassistant.auth.providers.homeassistant.Data.async_save", - ) as mock_save: - resp = await hassio_client.post( - "/api/hassio_auth/password_reset", - json={"username": "test", "password": "123456"}, - ) + resp = await hassio_client.post( + "/api/hassio_auth/password_reset", + json={"username": "test", "password": "123456"}, + ) - # Check we got right response - assert resp.status == 401 - assert not mock_save.called + # Check we got right response + assert resp.status == 401 async def test_password_fails_no_auth(hass, hassio_noauth_client): """Test if only supervisor can access.""" - with patch( - "homeassistant.auth.providers.homeassistant.Data.async_save", - ) as mock_save: - resp = await hassio_noauth_client.post( - "/api/hassio_auth/password_reset", - json={"username": "test", "password": "123456"}, - ) + resp = await hassio_noauth_client.post( + "/api/hassio_auth/password_reset", + json={"username": "test", "password": "123456"}, + ) - # Check we got right response - assert resp.status == 401 - assert not mock_save.called + # Check we got right response + assert resp.status == 401 async def test_password_no_user(hass, hassio_client_supervisor): - """Test no auth needed for .""" - with patch( - "homeassistant.auth.providers.homeassistant.Data.async_save", - ) as mock_save: - resp = await hassio_client_supervisor.post( - "/api/hassio_auth/password_reset", - json={"username": "test", "password": "123456"}, - ) + """Test changing password for invalid user.""" + resp = await hassio_client_supervisor.post( + "/api/hassio_auth/password_reset", + json={"username": "test", "password": "123456"}, + ) - # Check we got right response - assert resp.status == HTTP_INTERNAL_SERVER_ERROR - assert not mock_save.called + # Check we got right response + assert resp.status == 404 From 342e84e55028ff81feee7d362f63a4078b564b84 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 25 Aug 2020 14:23:21 +0200 Subject: [PATCH 332/862] Fix TTS languange characters (#39211) --- homeassistant/components/tts/__init__.py | 11 ++++++-- tests/components/tts/test_init.py | 35 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 948db0f5d46..d1e2d910790 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -324,7 +324,9 @@ class SpeechManager: else: options_key = "-" - key = KEY_PATTERN.format(msg_hash, language, options_key, engine).lower() + key = KEY_PATTERN.format( + msg_hash, language.replace("_", "-"), options_key, engine + ).lower() # Is speech already in memory if key in self.mem_cache: @@ -355,9 +357,14 @@ class SpeechManager: # Create file infos filename = f"{key}.{extension}".lower() - data = self.write_tags(filename, data, provider, message, language, options) + # Validate filename + if not _RE_VOICE_FILE.match(filename): + raise HomeAssistantError( + f"TTS filename '{filename}' from {engine} is invalid!" + ) # Save to memory + data = self.write_tags(filename, data, provider, message, language, options) self._async_store_to_memcache(key, filename, data) if cache: diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index b4cb9c67af3..a100ef22c65 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -176,6 +176,41 @@ async def test_setup_component_and_test_service_with_config_language( ).is_file() +async def test_setup_component_and_test_service_with_config_language_special( + hass, empty_cache_dir +): + """Set up the demo platform and call service with extend language.""" + import homeassistant.components.demo.tts as demo_tts + + demo_tts.SUPPORT_LANGUAGES.append("en_US") + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + config = {tts.DOMAIN: {"platform": "demo", "language": "en_US"}} + + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] + == "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_demo.mp3" + ) + await hass.async_block_till_done() + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_demo.mp3" + ).is_file() + + async def test_setup_component_and_test_service_with_wrong_conf_language(hass): """Set up the demo platform and call service with wrong config.""" config = {tts.DOMAIN: {"platform": "demo", "language": "ru"}} From 415213a325df9a6037a9b6bfcde16edf46411407 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 25 Aug 2020 16:21:16 +0200 Subject: [PATCH 333/862] Add support for attributes in state/numeric state trigger (#39238) --- .../homeassistant/triggers/numeric_state.py | 6 +- .../homeassistant/triggers/state.py | 46 +++++++---- .../triggers/test_numeric_state.py | 58 ++++++++++++++ .../homeassistant/triggers/test_state.py | 80 +++++++++++++++++++ 4 files changed, 175 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index b2958f4d63a..a1678e0a24c 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.const import ( CONF_ABOVE, + CONF_ATTRIBUTE, CONF_BELOW, CONF_ENTITY_ID, CONF_FOR, @@ -48,6 +49,7 @@ TRIGGER_SCHEMA = vol.All( vol.Optional(CONF_ABOVE): vol.Coerce(float), vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FOR): cv.positive_time_period_template, + vol.Optional(CONF_ATTRIBUTE): cv.match_all, } ), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), @@ -70,6 +72,7 @@ async def async_attach_trigger( unsub_track_same = {} entities_triggered = set() period: dict = {} + attribute = config.get(CONF_ATTRIBUTE) if value_template is not None: value_template.hass = hass @@ -86,10 +89,11 @@ async def async_attach_trigger( "entity_id": entity, "below": below, "above": above, + "attribute": attribute, } } return condition.async_numeric_state( - hass, to_s, below, above, value_template, variables + hass, to_s, below, above, value_template, variables, attribute ) @callback diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 9ddd1b3fcb8..7ccba9aade8 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -1,13 +1,13 @@ """Offer state listening automation rules.""" from datetime import timedelta import logging -from typing import Dict +from typing import Dict, Optional import voluptuous as vol from homeassistant import exceptions -from homeassistant.const import CONF_FOR, CONF_PLATFORM, MATCH_ALL -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_ALL +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import ( Event, @@ -34,6 +34,7 @@ TRIGGER_SCHEMA = vol.All( vol.Optional(CONF_FROM): vol.Any(str, [str]), vol.Optional(CONF_TO): vol.Any(str, [str]), vol.Optional(CONF_FOR): cv.positive_time_period_template, + vol.Optional(CONF_ATTRIBUTE): cv.match_all, } ), cv.key_dependency(CONF_FOR, CONF_TO), @@ -59,23 +60,33 @@ async def async_attach_trigger( period: Dict[str, timedelta] = {} match_from_state = process_state_match(from_state) match_to_state = process_state_match(to_state) + attribute = config.get(CONF_ATTRIBUTE) @callback def state_automation_listener(event: Event): """Listen for state changes and calls action.""" entity: str = event.data["entity_id"] - if entity not in entity_id: - return + from_s: Optional[State] = event.data.get("old_state") + to_s: Optional[State] = event.data.get("new_state") - from_s = event.data.get("old_state") - to_s = event.data.get("new_state") - old_state = getattr(from_s, "state", None) - new_state = getattr(to_s, "state", None) + if from_s is None: + old_value = None + elif attribute is None: + old_value = from_s.state + else: + old_value = from_s.attributes.get(attribute) + + if to_s is None: + new_value = None + elif attribute is None: + new_value = to_s.state + else: + new_value = to_s.attributes.get(attribute) if ( - not match_from_state(old_state) - or not match_to_state(new_state) - or (not match_all and old_state == new_state) + not match_from_state(old_value) + or not match_to_state(new_value) + or (not match_all and old_value == new_value) ): return @@ -91,6 +102,7 @@ async def async_attach_trigger( "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity], + "attribute": attribute, } }, event.context, @@ -119,10 +131,16 @@ async def async_attach_trigger( ) return - def _check_same_state(_, _2, new_st): + def _check_same_state(_, _2, new_st: State): if new_st is None: return False - return new_st.state == to_s.state + + if attribute is None: + cur_value = new_st.state + else: + cur_value = new_st.attributes.get(attribute) + + return cur_value == new_value unsub_track_same[entity] = async_track_same_state( hass, period[entity], call_action, _check_same_state, entity_ids=entity, diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 01b276c236a..932dde91120 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -1239,3 +1239,61 @@ def test_below_above(): numeric_state_trigger.TRIGGER_SCHEMA( {"platform": "numeric_state", "above": 1200, "below": 1000} ) + + +async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls): + """Test for firing if both filters are match attribute.""" + hass.states.async_set("test.entity", "bla", {"test-measurement": 1}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "numeric_state", + "entity_id": "test.entity", + "above": 3, + "attribute": "test-measurement", + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + hass.states.async_set("test.entity", "bla", {"test-measurement": 4}) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( + hass, calls +): + """Test for not firing on entity change with for after stop trigger.""" + hass.states.async_set("test.entity", "bla", {"test-measurement": 1}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "numeric_state", + "entity_id": "test.entity", + "above": 3, + "attribute": "test-measurement", + "for": 5, + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + hass.states.async_set("test.entity", "bla", {"test-measurement": 4}) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 7f256293025..9cce567ca68 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -1007,3 +1007,83 @@ async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "test.entity_2 - 0:00:10" + + +async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls): + """Test for firing if both filters are match attribute.""" + hass.states.async_set("test.entity", "bla", {"name": "hello"}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "from": "hello", + "to": "world", + "attribute": "name", + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + hass.states.async_set("test.entity", "bla", {"name": "world"}) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( + hass, calls +): + """Test for not firing on entity change with for after stop trigger.""" + hass.states.async_set("test.entity", "bla", {"name": "hello"}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "from": "hello", + "to": "world", + "attribute": "name", + "for": 5, + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + # Test that the for-check works + hass.states.async_set("test.entity", "bla", {"name": "world"}) + await hass.async_block_till_done() + assert len(calls) == 0 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + hass.states.async_set("test.entity", "bla", {"name": "world", "something": "else"}) + await hass.async_block_till_done() + assert len(calls) == 0 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + + # Now remove state while inside "for" + hass.states.async_set("test.entity", "bla", {"name": "hello"}) + hass.states.async_set("test.entity", "bla", {"name": "world"}) + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_remove("test.entity") + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 From 114a7226d68718c27a4155d854cbeb1926a89b19 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 25 Aug 2020 16:42:24 +0200 Subject: [PATCH 334/862] Wait before sending MQTT birth message (#39120) Co-authored-by: Paulus Schoutsen --- homeassistant/components/mqtt/__init__.py | 57 +++++++++++++++++++--- homeassistant/components/mqtt/discovery.py | 4 ++ tests/components/mqtt/test_init.py | 44 ++++++++++++----- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 819fbc9838f..865f21b9d38 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -8,6 +8,7 @@ import logging from operator import attrgetter import os import ssl +import time from typing import Any, Callable, List, Optional, Union import attr @@ -26,10 +27,11 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.const import CONF_UNIQUE_ID # noqa: F401 -from homeassistant.core import Event, ServiceCall, callback +from homeassistant.core import CoreState, Event, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send @@ -71,7 +73,12 @@ from .const import ( PROTOCOL_311, ) from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash +from .discovery import ( + LAST_DISCOVERY, + MQTT_DISCOVERY_UPDATED, + clear_discovery_hash, + set_discovery_hash, +) from .models import Message, MessageCallbackType, PublishPayloadType from .subscription import async_subscribe_topics, async_unsubscribe_topics from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic @@ -126,6 +133,7 @@ CONNECTION_SUCCESS = "connection_success" CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" +DISCOVERY_COOLDOWN = 2 TIMEOUT_ACK = 1 @@ -623,11 +631,23 @@ class MQTT: self.conf = conf self.subscriptions: List[Subscription] = [] self.connected = False + self._ha_started = asyncio.Event() + self._last_subscribe = time.time() self._mqttc: mqtt.Client = None self._paho_lock = asyncio.Lock() self._pending_operations = {} + if self.hass.state == CoreState.running: + self._ha_started.set() + else: + + @callback + def ha_started(_): + self._ha_started.set() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) + self.init_client() self.config_entry.add_update_listener(self.async_config_entry_updated) @@ -800,6 +820,7 @@ class MQTT: # Only subscribe if currently connected. if self.connected: + self._last_subscribe = time.time() await self._async_perform_subscription(topic, qos) @callback @@ -880,15 +901,19 @@ class MQTT: CONF_BIRTH_MESSAGE in self.conf and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE] ): - birth_message = Message(**self.conf[CONF_BIRTH_MESSAGE]) - self.hass.add_job( - self.async_publish( # pylint: disable=no-value-for-parameter + + async def publish_birth_message(birth_message): + await self._ha_started.wait() # Wait for Home Assistant to start + await self._discovery_cooldown() # Wait for MQTT discovery to cool down + await self.async_publish( # pylint: disable=no-value-for-parameter topic=birth_message.topic, payload=birth_message.payload, qos=birth_message.qos, retain=birth_message.retain, ) - ) + + birth_message = Message(**self.conf[CONF_BIRTH_MESSAGE]) + self.hass.loop.create_task(publish_birth_message(birth_message)) def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: """Message received callback.""" @@ -970,6 +995,26 @@ class MQTT: finally: del self._pending_operations[mid] + async def _discovery_cooldown(self): + now = time.time() + # Reset discovery and subscribe cooldowns + self.hass.data[LAST_DISCOVERY] = now + self._last_subscribe = now + + last_discovery = self.hass.data[LAST_DISCOVERY] + last_subscribe = self._last_subscribe + wait_until = max( + last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN + ) + while now < wait_until: + await asyncio.sleep(wait_until - now) + now = time.time() + last_discovery = self.hass.data[LAST_DISCOVERY] + last_subscribe = self._last_subscribe + wait_until = max( + last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN + ) + def _raise_on_error(result_code: int) -> None: """Raise error if error result.""" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index aff66954968..a7d5236148b 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -3,6 +3,7 @@ import asyncio import json import logging import re +import time from homeassistant.components import mqtt from homeassistant.const import CONF_DEVICE, CONF_PLATFORM @@ -40,6 +41,7 @@ DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DISCOVERY_UNSUBSCRIBE = "mqtt_discovery_unsubscribe" MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}" MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" +LAST_DISCOVERY = "mqtt_last_discovery" TOPIC_BASE = "~" @@ -65,6 +67,7 @@ async def async_start( async def async_device_message_received(msg): """Process the received message.""" + hass.data[LAST_DISCOVERY] = time.time() payload = msg.payload topic = msg.topic topic_trimmed = topic.replace(f"{discovery_topic}/", "", 1) @@ -167,6 +170,7 @@ async def async_start( hass.data[DISCOVERY_UNSUBSCRIBE] = await mqtt.async_subscribe( hass, f"{discovery_topic}/#", async_device_message_received, 0 ) + hass.data[LAST_DISCOVERY] = time.time() return True diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index dea0852d580..0dfef17a145 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,4 +1,5 @@ """The tests for the MQTT component.""" +import asyncio from datetime import datetime, timedelta import json import ssl @@ -361,7 +362,6 @@ async def test_subscribe_deprecated_async(hass, mqtt_mock): """Test the subscription of a topic using deprecated callback signature.""" calls = [] - @callback async def record_calls(topic, payload, qos): """Record calls.""" calls.append((topic, payload, qos)) @@ -758,18 +758,36 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): ) async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test sending birth message.""" - mqtt_mock._mqtt_on_connect(None, None, 0, 0) - await hass.async_block_till_done() - mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) + birth = asyncio.Event() + + async def wait_birth(topic, payload, qos): + """Handle birth message.""" + birth.set() + + with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + await mqtt.async_subscribe(hass, "birth", wait_birth) + mqtt_mock._mqtt_on_connect(None, None, 0, 0) + await hass.async_block_till_done() + await birth.wait() + mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test sending birth message.""" - mqtt_mock._mqtt_on_connect(None, None, 0, 0) - await hass.async_block_till_done() - mqtt_client_mock.publish.assert_called_with( - "homeassistant/status", "online", 0, False - ) + birth = asyncio.Event() + + async def wait_birth(topic, payload, qos): + """Handle birth message.""" + birth.set() + + with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + mqtt_mock._mqtt_on_connect(None, None, 0, 0) + await hass.async_block_till_done() + await birth.wait() + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) @pytest.mark.parametrize( @@ -777,9 +795,11 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): ) async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test disabling birth message.""" - mqtt_mock._mqtt_on_connect(None, None, 0, 0) - await hass.async_block_till_done() - mqtt_client_mock.publish.assert_not_called() + with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + mqtt_mock._mqtt_on_connect(None, None, 0, 0) + await hass.async_block_till_done() + await asyncio.sleep(0.2) + mqtt_client_mock.publish.assert_not_called() @pytest.mark.parametrize( From ab6fb5cb7705a7f2e3a4f310b5d42047c3372bd2 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 25 Aug 2020 11:34:14 -0500 Subject: [PATCH 335/862] Ensure unique ids are generated for surepetcare (#39196) * ensure unique ids are generated for surepetcare * Create test_binary_sensor.py * work on tests * Update test_binary_sensor.py * Update __init__.py * Update __init__.py * Update test_binary_sensor.py * Update test_binary_sensor.py * Update test_binary_sensor.py * Update test_binary_sensor.py * Update test_binary_sensor.py * Update test_binary_sensor.py * Update __init__.py --- .coveragerc | 3 +- .../components/surepetcare/__init__.py | 4 +- requirements_test_all.txt | 3 + tests/components/surepetcare/__init__.py | 88 +++++++++++++++++++ tests/components/surepetcare/conftest.py | 23 +++++ .../surepetcare/test_binary_sensor.py | 32 +++++++ 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tests/components/surepetcare/__init__.py create mode 100644 tests/components/surepetcare/conftest.py create mode 100644 tests/components/surepetcare/test_binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 8c67762250e..6566b4caad6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -814,7 +814,8 @@ omit = homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py - homeassistant/components/surepetcare/*.py + homeassistant/components/surepetcare/__init__.py + homeassistant/components/surepetcare/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swisscom/device_tracker.py diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 90e754118ab..9aac8f11941 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -104,12 +104,13 @@ async def async_setup(hass, config) -> bool: ) # discover hubs the flaps/feeders are connected to + hub_ids = set() for device in things.copy(): device_data = await surepy.device(device[CONF_ID]) if ( CONF_PARENT in device_data and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SureProductID.HUB - and device_data[CONF_PARENT][CONF_ID] not in things + and device_data[CONF_PARENT][CONF_ID] not in hub_ids ): things.append( { @@ -117,6 +118,7 @@ async def async_setup(hass, config) -> bool: CONF_TYPE: SureProductID.HUB, } ) + hub_ids.add(device_data[CONF_PARENT][CONF_ID]) # add pets things.extend( diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 758b78191e8..28739d751da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -960,6 +960,9 @@ stringcase==1.2.0 # homeassistant.components.solarlog sunwatcher==0.2.1 +# homeassistant.components.surepetcare +surepy==0.2.5 + # homeassistant.components.tellduslive tellduslive==0.10.11 diff --git a/tests/components/surepetcare/__init__.py b/tests/components/surepetcare/__init__.py new file mode 100644 index 00000000000..a7ce6d3b6a6 --- /dev/null +++ b/tests/components/surepetcare/__init__.py @@ -0,0 +1,88 @@ +"""Tests for Sure Petcare integration.""" +from homeassistant.components.surepetcare.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.async_mock import patch + +HOUSEHOLD_ID = "household-id" +HUB_ID = "hub-id" + +MOCK_HUB = { + "id": HUB_ID, + "product_id": 1, + "household_id": HOUSEHOLD_ID, + "name": "Hub", + "status": {"online": True, "led_mode": 0, "pairing_mode": 0}, +} + +MOCK_FEEDER = { + "id": 12345, + "product_id": 4, + "household_id": HOUSEHOLD_ID, + "name": "Feeder", + "parent": {"product_id": 1, "id": HUB_ID}, + "status": { + "battery": 6.4, + "locking": {"mode": 0}, + "learn_mode": 0, + "signal": {"device_rssi": 60, "hub_rssi": 65}, + }, +} + +MOCK_CAT_FLAP = { + "id": 13579, + "product_id": 6, + "household_id": HOUSEHOLD_ID, + "name": "Cat Flap", + "parent": {"product_id": 1, "id": HUB_ID}, + "status": { + "battery": 6.4, + "locking": {"mode": 0}, + "learn_mode": 0, + "signal": {"device_rssi": 65, "hub_rssi": 64}, + }, +} + +MOCK_PET_FLAP = { + "id": 13576, + "product_id": 3, + "household_id": HOUSEHOLD_ID, + "name": "Pet Flap", + "parent": {"product_id": 1, "id": HUB_ID}, + "status": { + "battery": 6.4, + "locking": {"mode": 0}, + "learn_mode": 0, + "signal": {"device_rssi": 70, "hub_rssi": 65}, + }, +} + +MOCK_PET = { + "id": 24680, + "household_id": HOUSEHOLD_ID, + "name": "Pet", + "position": {"since": "2020-08-23T23:10:50", "where": 1}, + "status": {}, +} + +MOCK_API_DATA = { + "devices": [MOCK_HUB, MOCK_CAT_FLAP, MOCK_PET_FLAP, MOCK_FEEDER], + "pets": [MOCK_PET], +} + +MOCK_CONFIG = { + DOMAIN: { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + "feeders": [12345], + "flaps": [13579, 13576], + "pets": [24680], + }, +} + + +def _patch_sensor_setup(): + return patch( + "homeassistant.components.surepetcare.sensor.async_setup_platform", + return_value=True, + ) diff --git a/tests/components/surepetcare/conftest.py b/tests/components/surepetcare/conftest.py new file mode 100644 index 00000000000..6d85a2b0189 --- /dev/null +++ b/tests/components/surepetcare/conftest.py @@ -0,0 +1,23 @@ +"""Define fixtures available for all tests.""" +from pytest import fixture +from surepy import SurePetcare + +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from tests.async_mock import AsyncMock, patch + + +@fixture() +def surepetcare(hass): + """Mock the SurePetcare for easier testing.""" + with patch("homeassistant.components.surepetcare.SurePetcare") as mock_surepetcare: + instance = mock_surepetcare.return_value = SurePetcare( + "test-username", + "test-password", + hass.loop, + async_get_clientsession(hass), + api_timeout=1, + ) + instance.get_data = AsyncMock(return_value=None) + + yield mock_surepetcare diff --git a/tests/components/surepetcare/test_binary_sensor.py b/tests/components/surepetcare/test_binary_sensor.py new file mode 100644 index 00000000000..9478ca7a1d4 --- /dev/null +++ b/tests/components/surepetcare/test_binary_sensor.py @@ -0,0 +1,32 @@ +"""The tests for the Sure Petcare binary sensor platform.""" +from homeassistant.components.surepetcare.const import DOMAIN +from homeassistant.setup import async_setup_component + +from . import MOCK_API_DATA, MOCK_CONFIG, _patch_sensor_setup + +EXPECTED_ENTITY_IDS = { + "binary_sensor.pet_flap_pet_flap_connectivity": "household-id-13576-connectivity", + "binary_sensor.pet_flap_cat_flap_connectivity": "household-id-13579-connectivity", + "binary_sensor.feeder_feeder_connectivity": "household-id-12345-connectivity", + "binary_sensor.pet_pet": "household-id-24680", + "binary_sensor.hub_hub": "household-id-hub-id", +} + + +async def test_binary_sensors(hass, surepetcare) -> None: + """Test the generation of unique ids.""" + instance = surepetcare.return_value + instance.data = MOCK_API_DATA + instance.get_data.return_value = MOCK_API_DATA + + with _patch_sensor_setup(): + assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + state_entity_ids = hass.states.async_entity_ids() + + for entity_id, unique_id in EXPECTED_ENTITY_IDS.items(): + assert entity_id in state_entity_ids + entity = entity_registry.async_get(entity_id) + assert entity.unique_id == unique_id From 19cc168433f137818a8dc9ed4a6c64567ece253e Mon Sep 17 00:00:00 2001 From: Yuxiang Zhu Date: Wed, 26 Aug 2020 00:56:01 +0800 Subject: [PATCH 336/862] Add HomeKit Controller heater-cooler devices (#38979) Some new HomeKit climate devices, like XiaoMi Air Conditioning Controller P3 are heater-cooler devices rather than thermostat devices. This commit adds support for the heater-cooler class via homekit_controller. --- .../components/homekit_controller/climate.py | 270 ++++++++++++++++- .../components/homekit_controller/const.py | 1 + .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_climate.py | 285 +++++++++++++++++- 6 files changed, 553 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index f06063c5fd2..8551f4dddd5 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -2,9 +2,13 @@ import logging from aiohomekit.model.characteristics import ( + ActivationStateValues, CharacteristicsTypes, + CurrentHeaterCoolerStateValues, HeatingCoolingCurrentValues, HeatingCoolingTargetValues, + SwingModeValues, + TargetHeaterCoolerStateValues, ) from aiohomekit.utils import clamp_enum_to_char @@ -17,12 +21,16 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, + SWING_OFF, + SWING_VERTICAL, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback @@ -39,15 +47,39 @@ MODE_HOMEKIT_TO_HASS = { HeatingCoolingTargetValues.AUTO: HVAC_MODE_HEAT_COOL, } -# Map of hass operation modes to homekit modes -MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} - CURRENT_MODE_HOMEKIT_TO_HASS = { HeatingCoolingCurrentValues.IDLE: CURRENT_HVAC_IDLE, HeatingCoolingCurrentValues.HEATING: CURRENT_HVAC_HEAT, HeatingCoolingCurrentValues.COOLING: CURRENT_HVAC_COOL, } +SWING_MODE_HOMEKIT_TO_HASS = { + SwingModeValues.DISABLED: SWING_OFF, + SwingModeValues.ENABLED: SWING_VERTICAL, +} + +CURRENT_HEATER_COOLER_STATE_HOMEKIT_TO_HASS = { + CurrentHeaterCoolerStateValues.INACTIVE: CURRENT_HVAC_OFF, + CurrentHeaterCoolerStateValues.IDLE: CURRENT_HVAC_IDLE, + CurrentHeaterCoolerStateValues.HEATING: CURRENT_HVAC_HEAT, + CurrentHeaterCoolerStateValues.COOLING: CURRENT_HVAC_COOL, +} + +TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS = { + TargetHeaterCoolerStateValues.AUTOMATIC: HVAC_MODE_HEAT_COOL, + TargetHeaterCoolerStateValues.HEAT: HVAC_MODE_HEAT, + TargetHeaterCoolerStateValues.COOL: HVAC_MODE_COOL, +} + +# Map of hass operation modes to homekit modes +MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} + +TARGET_HEATER_COOLER_STATE_HASS_TO_HOMEKIT = { + v: k for k, v in TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.items() +} + +SWING_MODE_HASS_TO_HOMEKIT = {v: k for k, v in SWING_MODE_HOMEKIT_TO_HASS.items()} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit climate.""" @@ -56,15 +88,237 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(aid, service): - if service["stype"] != "thermostat": + entity_class = ENTITY_TYPES.get(service["stype"]) + if not entity_class: return False info = {"aid": aid, "iid": service["iid"]} - async_add_entities([HomeKitClimateEntity(conn, info)], True) + async_add_entities([entity_class(conn, info)], True) return True conn.add_listener(async_add_service) +class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): + """Representation of a Homekit climate device.""" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.ACTIVE, + CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, + CharacteristicsTypes.SWING_MODE, + CharacteristicsTypes.TEMPERATURE_CURRENT, + ] + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) + if state == TargetHeaterCoolerStateValues.COOL: + await self.async_put_characteristics( + {CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: temp} + ) + elif state == TargetHeaterCoolerStateValues.HEAT: + await self.async_put_characteristics( + {CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: temp} + ) + else: + hvac_mode = TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.get(state) + _LOGGER.warning( + "HomeKit device %s: Setting temperature in %s mode is not supported yet." + " Consider raising a ticket if you have this device and want to help us implement this feature.", + self.entity_id, + hvac_mode, + ) + + async def async_set_hvac_mode(self, hvac_mode): + """Set new target operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self.async_put_characteristics( + {CharacteristicsTypes.ACTIVE: ActivationStateValues.INACTIVE} + ) + return + if hvac_mode not in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: + _LOGGER.warning( + "HomeKit device %s: Setting temperature in %s mode is not supported yet." + " Consider raising a ticket if you have this device and want to help us implement this feature.", + self.entity_id, + hvac_mode, + ) + await self.async_put_characteristics( + { + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TARGET_HEATER_COOLER_STATE_HASS_TO_HOMEKIT[ + hvac_mode + ], + } + ) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) + if state == TargetHeaterCoolerStateValues.COOL: + return self.service.value( + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ) + if state == TargetHeaterCoolerStateValues.HEAT: + return self.service.value( + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ) + return None + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) + if state == TargetHeaterCoolerStateValues.COOL and self.service.has( + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ): + return self.service[ + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ].minStep + if state == TargetHeaterCoolerStateValues.HEAT and self.service.has( + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ): + return self.service[ + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ].minStep + return None + + @property + def min_temp(self): + """Return the minimum target temp.""" + state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) + if state == TargetHeaterCoolerStateValues.COOL and self.service.has( + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ): + return self.service[ + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ].minValue + if state == TargetHeaterCoolerStateValues.HEAT and self.service.has( + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ): + return self.service[ + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ].minValue + return super().min_temp + + @property + def max_temp(self): + """Return the maximum target temp.""" + state = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) + if state == TargetHeaterCoolerStateValues.COOL and self.service.has( + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ): + return self.service[ + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ].maxValue + if state == TargetHeaterCoolerStateValues.HEAT and self.service.has( + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ): + return self.service[ + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ].maxValue + return super().max_temp + + @property + def hvac_action(self): + """Return the current running hvac operation.""" + # This characteristic describes the current mode of a device, + # e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit. + # Can be 0 - 3 (Off, Idle, Heat, Cool) + if ( + self.service.value(CharacteristicsTypes.ACTIVE) + == ActivationStateValues.INACTIVE + ): + return CURRENT_HVAC_OFF + value = self.service.value(CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE) + return CURRENT_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.get(value) + + @property + def hvac_mode(self): + """Return hvac operation ie. heat, cool mode.""" + # This characteristic describes the target mode + # E.g. should the device start heating a room if the temperature + # falls below the target temperature. + # Can be 0 - 2 (Auto, Heat, Cool) + if ( + self.service.value(CharacteristicsTypes.ACTIVE) + == ActivationStateValues.INACTIVE + ): + return HVAC_MODE_OFF + value = self.service.value(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) + return TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS.get(value) + + @property + def hvac_modes(self): + """Return the list of available hvac operation modes.""" + valid_values = clamp_enum_to_char( + TargetHeaterCoolerStateValues, + self.service[CharacteristicsTypes.TARGET_HEATER_COOLER_STATE], + ) + modes = [ + TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS[mode] for mode in valid_values + ] + modes.append(HVAC_MODE_OFF) + return modes + + @property + def swing_mode(self): + """Return the swing setting. + + Requires SUPPORT_SWING_MODE. + """ + value = self.service.value(CharacteristicsTypes.SWING_MODE) + return SWING_MODE_HOMEKIT_TO_HASS[value] + + @property + def swing_modes(self): + """Return the list of available swing modes. + + Requires SUPPORT_SWING_MODE. + """ + valid_values = clamp_enum_to_char( + SwingModeValues, self.service[CharacteristicsTypes.SWING_MODE], + ) + return [SWING_MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values] + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing operation.""" + await self.async_put_characteristics( + {CharacteristicsTypes.SWING_MODE: SWING_MODE_HASS_TO_HOMEKIT[swing_mode]} + ) + + @property + def supported_features(self): + """Return the list of supported features.""" + features = 0 + + if self.service.has(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD): + features |= SUPPORT_TARGET_TEMPERATURE + + if self.service.has(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD): + features |= SUPPORT_TARGET_TEMPERATURE + + if self.service.has(CharacteristicsTypes.SWING_MODE): + features |= SUPPORT_SWING_MODE + + return features + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): """Representation of a Homekit climate device.""" @@ -196,3 +450,9 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): def temperature_unit(self): """Return the unit of measurement.""" return TEMP_CELSIUS + + +ENTITY_TYPES = { + "heater-cooler": HomeKitHeaterCoolerEntity, + "thermostat": HomeKitClimateEntity, +} diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 7b40863141c..394750c0688 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -14,6 +14,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { "outlet": "switch", "switch": "switch", "thermostat": "climate", + "heater-cooler": "climate", "security-system": "alarm_control_panel", "garage-door-opener": "cover", "window": "cover", diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a915f84510a..1ee2f16ffcf 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit[IP]==0.2.47"], + "requirements": ["aiohomekit[IP]==0.2.49"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k"] diff --git a/requirements_all.txt b/requirements_all.txt index 3785cff85df..3ec08978c82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.47 +aiohomekit[IP]==0.2.49 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28739d751da..84f89b8c878 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -104,7 +104,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.47 +aiohomekit[IP]==0.2.49 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 9bcadb6604e..38156354cda 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -1,5 +1,11 @@ """Basic checks for HomeKitclimate.""" -from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics import ( + ActivationStateValues, + CharacteristicsTypes, + CurrentHeaterCoolerStateValues, + SwingModeValues, + TargetHeaterCoolerStateValues, +) from aiohomekit.model.services import ServicesTypes from homeassistant.components.climate.const import ( @@ -10,6 +16,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, + SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, ) @@ -22,6 +29,8 @@ TEMPERATURE_CURRENT = ("thermostat", "temperature.current") HUMIDITY_TARGET = ("thermostat", "relative-humidity.target") HUMIDITY_CURRENT = ("thermostat", "relative-humidity.current") +# Test thermostat devices + def create_thermostat_service(accessory): """Define thermostat characteristics.""" @@ -226,3 +235,277 @@ async def test_hvac_mode_vs_hvac_action(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "heat" assert state.attributes["hvac_action"] == "heating" + + +TARGET_HEATER_COOLER_STATE = ("heater-cooler", "heater-cooler.state.target") +CURRENT_HEATER_COOLER_STATE = ("heater-cooler", "heater-cooler.state.current") +HEATER_COOLER_ACTIVE = ("heater-cooler", "active") +HEATER_COOLER_TEMPERATURE_CURRENT = ("heater-cooler", "temperature.current") +TEMPERATURE_COOLING_THRESHOLD = ("heater-cooler", "temperature.cooling-threshold") +TEMPERATURE_HEATING_THRESHOLD = ("heater-cooler", "temperature.heating-threshold") +SWING_MODE = ("heater-cooler", "swing-mode") + + +def create_heater_cooler_service(accessory): + """Define thermostat characteristics.""" + service = accessory.add_service(ServicesTypes.HEATER_COOLER) + + char = service.add_char(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) + char.value = 0 + + char = service.add_char(CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE) + char.value = 0 + + char = service.add_char(CharacteristicsTypes.ACTIVE) + char.value = 1 + + char = service.add_char(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD) + char.minValue = 7 + char.maxValue = 35 + char.value = 0 + + char = service.add_char(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD) + char.minValue = 7 + char.maxValue = 35 + char.value = 0 + + char = service.add_char(CharacteristicsTypes.TEMPERATURE_CURRENT) + char.value = 0 + + char = service.add_char(CharacteristicsTypes.SWING_MODE) + char.value = 0 + + +# Test heater-cooler devices +def create_heater_cooler_service_min_max(accessory): + """Define thermostat characteristics.""" + service = accessory.add_service(ServicesTypes.HEATER_COOLER) + char = service.add_char(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) + char.value = 1 + char.minValue = 1 + char.maxValue = 2 + + +async def test_heater_cooler_respect_supported_op_modes_1(hass, utcnow): + """Test that climate respects minValue/maxValue hints.""" + helper = await setup_test_component(hass, create_heater_cooler_service_min_max) + state = await helper.poll_and_get_state() + assert state.attributes["hvac_modes"] == ["heat", "cool", "off"] + + +def create_theater_cooler_service_valid_vals(accessory): + """Define heater-cooler characteristics.""" + service = accessory.add_service(ServicesTypes.HEATER_COOLER) + char = service.add_char(CharacteristicsTypes.TARGET_HEATER_COOLER_STATE) + char.value = 1 + char.valid_values = [1, 2] + + +async def test_heater_cooler_respect_supported_op_modes_2(hass, utcnow): + """Test that climate respects validValue hints.""" + helper = await setup_test_component(hass, create_theater_cooler_service_valid_vals) + state = await helper.poll_and_get_state() + assert state.attributes["hvac_modes"] == ["heat", "cool", "off"] + + +async def test_heater_cooler_change_thermostat_state(hass, utcnow): + """Test that we can change the operational mode.""" + helper = await setup_test_component(hass, create_heater_cooler_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + + assert ( + helper.characteristics[TARGET_HEATER_COOLER_STATE].value + == TargetHeaterCoolerStateValues.HEAT + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_COOL}, + blocking=True, + ) + assert ( + helper.characteristics[TARGET_HEATER_COOLER_STATE].value + == TargetHeaterCoolerStateValues.COOL + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, + blocking=True, + ) + assert ( + helper.characteristics[TARGET_HEATER_COOLER_STATE].value + == TargetHeaterCoolerStateValues.AUTOMATIC + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_OFF}, + blocking=True, + ) + assert ( + helper.characteristics[HEATER_COOLER_ACTIVE].value + == ActivationStateValues.INACTIVE + ) + + +async def test_heater_cooler_change_thermostat_temperature(hass, utcnow): + """Test that we can change the target temperature.""" + helper = await setup_test_component(hass, create_heater_cooler_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {"entity_id": "climate.testdevice", "temperature": 20}, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_HEATING_THRESHOLD].value == 20 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_COOL}, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {"entity_id": "climate.testdevice", "temperature": 26}, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_COOLING_THRESHOLD].value == 26 + + +async def test_heater_cooler_read_thermostat_state(hass, utcnow): + """Test that we can read the state of a HomeKit thermostat accessory.""" + helper = await setup_test_component(hass, create_heater_cooler_service) + + # Simulate that heating is on + helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 19 + helper.characteristics[TEMPERATURE_HEATING_THRESHOLD].value = 20 + helper.characteristics[ + CURRENT_HEATER_COOLER_STATE + ].value = CurrentHeaterCoolerStateValues.HEATING + helper.characteristics[ + TARGET_HEATER_COOLER_STATE + ].value = TargetHeaterCoolerStateValues.HEAT + helper.characteristics[SWING_MODE].value = SwingModeValues.DISABLED + + state = await helper.poll_and_get_state() + assert state.state == HVAC_MODE_HEAT + assert state.attributes["current_temperature"] == 19 + assert state.attributes["min_temp"] == 7 + assert state.attributes["max_temp"] == 35 + + # Simulate that cooling is on + helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 21 + helper.characteristics[TEMPERATURE_COOLING_THRESHOLD].value = 19 + helper.characteristics[ + CURRENT_HEATER_COOLER_STATE + ].value = CurrentHeaterCoolerStateValues.COOLING + helper.characteristics[ + TARGET_HEATER_COOLER_STATE + ].value = TargetHeaterCoolerStateValues.COOL + helper.characteristics[SWING_MODE].value = SwingModeValues.DISABLED + + state = await helper.poll_and_get_state() + assert state.state == HVAC_MODE_COOL + assert state.attributes["current_temperature"] == 21 + + # Simulate that we are in auto mode + helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 21 + helper.characteristics[TEMPERATURE_COOLING_THRESHOLD].value = 21 + helper.characteristics[ + CURRENT_HEATER_COOLER_STATE + ].value = CurrentHeaterCoolerStateValues.COOLING + helper.characteristics[ + TARGET_HEATER_COOLER_STATE + ].value = TargetHeaterCoolerStateValues.AUTOMATIC + helper.characteristics[SWING_MODE].value = SwingModeValues.DISABLED + + state = await helper.poll_and_get_state() + assert state.state == HVAC_MODE_HEAT_COOL + + +async def test_heater_cooler_hvac_mode_vs_hvac_action(hass, utcnow): + """Check that we haven't conflated hvac_mode and hvac_action.""" + helper = await setup_test_component(hass, create_heater_cooler_service) + + # Simulate that current temperature is above target temp + # Heating might be on, but hvac_action currently 'off' + helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 22 + helper.characteristics[TEMPERATURE_HEATING_THRESHOLD].value = 21 + helper.characteristics[ + CURRENT_HEATER_COOLER_STATE + ].value = CurrentHeaterCoolerStateValues.IDLE + helper.characteristics[ + TARGET_HEATER_COOLER_STATE + ].value = TargetHeaterCoolerStateValues.HEAT + helper.characteristics[SWING_MODE].value = SwingModeValues.DISABLED + + state = await helper.poll_and_get_state() + assert state.state == "heat" + assert state.attributes["hvac_action"] == "idle" + + # Simulate that current temperature is below target temp + # Heating might be on and hvac_action currently 'heat' + helper.characteristics[HEATER_COOLER_TEMPERATURE_CURRENT].value = 19 + helper.characteristics[ + CURRENT_HEATER_COOLER_STATE + ].value = CurrentHeaterCoolerStateValues.HEATING + + state = await helper.poll_and_get_state() + assert state.state == "heat" + assert state.attributes["hvac_action"] == "heating" + + +async def test_heater_cooler_change_swing_mode(hass, utcnow): + """Test that we can change the swing mode.""" + helper = await setup_test_component(hass, create_heater_cooler_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SWING_MODE, + {"entity_id": "climate.testdevice", "swing_mode": "vertical"}, + blocking=True, + ) + assert helper.characteristics[SWING_MODE].value == SwingModeValues.ENABLED + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SWING_MODE, + {"entity_id": "climate.testdevice", "swing_mode": "off"}, + blocking=True, + ) + assert helper.characteristics[SWING_MODE].value == SwingModeValues.DISABLED + + +async def test_heater_cooler_turn_off(hass, utcnow): + """Test that both hvac_action and hvac_mode return "off" when turned off.""" + helper = await setup_test_component(hass, create_heater_cooler_service) + # Simulate that the device is turned off but CURRENT_HEATER_COOLER_STATE still returns HEATING/COOLING + helper.characteristics[HEATER_COOLER_ACTIVE].value = ActivationStateValues.INACTIVE + helper.characteristics[ + CURRENT_HEATER_COOLER_STATE + ].value = CurrentHeaterCoolerStateValues.HEATING + helper.characteristics[ + TARGET_HEATER_COOLER_STATE + ].value = TargetHeaterCoolerStateValues.HEAT + state = await helper.poll_and_get_state() + assert state.state == "off" + assert state.attributes["hvac_action"] == "off" From 7bc273b182b8ff94172fe653206a0c7190ed85a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 25 Aug 2020 21:18:45 +0200 Subject: [PATCH 337/862] Remove yr integration after a request from yr.no (#39247) --- CODEOWNERS | 1 - homeassistant/components/yr/__init__.py | 1 - homeassistant/components/yr/manifest.json | 7 - homeassistant/components/yr/sensor.py | 281 ----- requirements_all.txt | 1 - requirements_test_all.txt | 1 - tests/components/yr/__init__.py | 1 - tests/components/yr/test_sensor.py | 127 --- tests/fixtures/yr.no.xml | 1184 --------------------- 9 files changed, 1604 deletions(-) delete mode 100644 homeassistant/components/yr/__init__.py delete mode 100644 homeassistant/components/yr/manifest.json delete mode 100644 homeassistant/components/yr/sensor.py delete mode 100644 tests/components/yr/__init__.py delete mode 100644 tests/components/yr/test_sensor.py delete mode 100644 tests/fixtures/yr.no.xml diff --git a/CODEOWNERS b/CODEOWNERS index eb9c736bc5e..84869fe7144 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -492,7 +492,6 @@ homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf homeassistant/components/yi/* @bachya -homeassistant/components/yr/* @danielhiversen homeassistant/components/zeroconf/* @Kane610 homeassistant/components/zerproc/* @emlove homeassistant/components/zha/* @dmulcahey @adminiuga diff --git a/homeassistant/components/yr/__init__.py b/homeassistant/components/yr/__init__.py deleted file mode 100644 index 8d33bd56d43..00000000000 --- a/homeassistant/components/yr/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The yr component.""" diff --git a/homeassistant/components/yr/manifest.json b/homeassistant/components/yr/manifest.json deleted file mode 100644 index f21248c9632..00000000000 --- a/homeassistant/components/yr/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "yr", - "name": "Yr", - "documentation": "https://www.home-assistant.io/integrations/yr", - "requirements": ["xmltodict==0.12.0"], - "codeowners": ["@danielhiversen"] -} diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py deleted file mode 100644 index 8d7a91f24da..00000000000 --- a/homeassistant/components/yr/sensor.py +++ /dev/null @@ -1,281 +0,0 @@ -"""Support for Yr.no weather service.""" -import asyncio -import logging -from random import randrange -from xml.parsers.expat import ExpatError - -import aiohttp -import async_timeout -import voluptuous as vol -import xmltodict - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_ELEVATION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - DEGREE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - HTTP_BAD_REQUEST, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, - UNIT_PERCENTAGE, -) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_call_later, async_track_utc_time_change -from homeassistant.util import dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = ( - "Weather forecast from met.no, delivered by the Norwegian " - "Meteorological Institute." -) -# https://api.met.no/license_data.html - -SENSOR_TYPES = { - "symbol": ["Symbol", None, None], - "precipitation": ["Precipitation", "mm", None], - "temperature": ["Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE], - "windSpeed": ["Wind speed", SPEED_METERS_PER_SECOND, None], - "windGust": ["Wind gust", SPEED_METERS_PER_SECOND, None], - "pressure": ["Pressure", PRESSURE_HPA, DEVICE_CLASS_PRESSURE], - "windDirection": ["Wind direction", DEGREE, None], - "humidity": ["Humidity", UNIT_PERCENTAGE, DEVICE_CLASS_HUMIDITY], - "fog": ["Fog", UNIT_PERCENTAGE, None], - "cloudiness": ["Cloudiness", UNIT_PERCENTAGE, None], - "lowClouds": ["Low clouds", UNIT_PERCENTAGE, None], - "mediumClouds": ["Medium clouds", UNIT_PERCENTAGE, None], - "highClouds": ["High clouds", UNIT_PERCENTAGE, None], - "dewpointTemperature": [ - "Dewpoint temperature", - TEMP_CELSIUS, - DEVICE_CLASS_TEMPERATURE, - ], -} - -CONF_FORECAST = "forecast" - -DEFAULT_FORECAST = 0 -DEFAULT_NAME = "yr" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_ELEVATION): vol.Coerce(int), - vol.Optional(CONF_FORECAST, default=DEFAULT_FORECAST): vol.Coerce(int), - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_MONITORED_CONDITIONS, default=["symbol"]): vol.All( - cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Yr.no sensor.""" - elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0) - forecast = config.get(CONF_FORECAST) - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - name = config.get(CONF_NAME) - - if None in (latitude, longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False - - coordinates = {"lat": str(latitude), "lon": str(longitude), "msl": str(elevation)} - - dev = [] - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(YrSensor(name, sensor_type)) - - weather = YrData(hass, coordinates, forecast, dev) - async_track_utc_time_change( - hass, weather.updating_devices, minute=randrange(60), second=0 - ) - await weather.fetching_data() - async_add_entities(dev) - - -class YrSensor(Entity): - """Representation of an Yr.no sensor.""" - - def __init__(self, name, sensor_type): - """Initialize the sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] - self.type = sensor_type - self._state = None - self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._device_class = SENSOR_TYPES[self.type][2] - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def entity_picture(self): - """Weather symbol if type is symbol.""" - if self.type != "symbol": - return None - return ( - "https://api.met.no/weatherapi/weathericon/1.1/" - f"?symbol={self._state};content_type=image/png" - ) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def device_class(self): - """Return the device class of this entity, if any.""" - return self._device_class - - -class YrData: - """Get the latest data and updates the states.""" - - def __init__(self, hass, coordinates, forecast, devices): - """Initialize the data object.""" - self._url = ( - "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/" - ) - self._urlparams = coordinates - self._forecast = forecast - self.devices = devices - self.data = {} - self.hass = hass - - async def fetching_data(self, *_): - """Get the latest data from yr.no.""" - - def try_again(err: str): - """Retry in 15 to 20 minutes.""" - minutes = 15 + randrange(6) - _LOGGER.error("Retrying in %i minutes: %s", minutes, err) - async_call_later(self.hass, minutes * 60, self.fetching_data) - - try: - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(10): - resp = await websession.get(self._url, params=self._urlparams) - if resp.status >= HTTP_BAD_REQUEST: - try_again(f"{resp.url} returned {resp.status}") - return - text = await resp.text() - - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - try_again(err) - return - - try: - self.data = xmltodict.parse(text)["weatherdata"] - except (ExpatError, IndexError) as err: - try_again(err) - return - - await self.updating_devices() - async_call_later(self.hass, 60 * 60, self.fetching_data) - - async def updating_devices(self, *_): - """Find the current data from self.data.""" - if not self.data: - return - - now = dt_util.utcnow() - forecast_time = now + dt_util.dt.timedelta(hours=self._forecast) - - # Find the correct time entry. Since not all time entries contain all - # types of data, we cannot just select one. Instead, we order them by - # distance from the desired forecast_time, and for every device iterate - # them in order of increasing distance, taking the first time_point - # that contains the desired data. - - ordered_entries = [] - - for time_entry in self.data["product"]["time"]: - valid_from = dt_util.parse_datetime(time_entry["@from"]) - valid_to = dt_util.parse_datetime(time_entry["@to"]) - - if now >= valid_to: - # Has already passed. Never select this. - continue - - average_dist = abs((valid_to - forecast_time).total_seconds()) + abs( - (valid_from - forecast_time).total_seconds() - ) - - ordered_entries.append((average_dist, time_entry)) - - ordered_entries.sort(key=lambda item: item[0]) - - # Update all devices - if ordered_entries: - for dev in self.devices: - new_state = None - - for (_, selected_time_entry) in ordered_entries: - loc_data = selected_time_entry["location"] - - if dev.type not in loc_data: - continue - - if dev.type == "precipitation": - new_state = loc_data[dev.type]["@value"] - elif dev.type == "symbol": - new_state = loc_data[dev.type]["@number"] - elif dev.type in ( - "temperature", - "pressure", - "humidity", - "dewpointTemperature", - ): - new_state = loc_data[dev.type]["@value"] - elif dev.type in ("windSpeed", "windGust"): - new_state = loc_data[dev.type]["@mps"] - elif dev.type == "windDirection": - new_state = float(loc_data[dev.type]["@deg"]) - elif dev.type in ( - "fog", - "cloudiness", - "lowClouds", - "mediumClouds", - "highClouds", - ): - new_state = loc_data[dev.type]["@percent"] - - break - - # pylint: disable=protected-access - if new_state != dev._state: - dev._state = new_state - if dev.hass: - dev.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 3ec08978c82..d2f43817c20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2268,7 +2268,6 @@ xknx==0.11.3 # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 -# homeassistant.components.yr # homeassistant.components.zestimate xmltodict==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84f89b8c878..f1815470aa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1031,7 +1031,6 @@ wolf_smartset==0.1.4 # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 -# homeassistant.components.yr # homeassistant.components.zestimate xmltodict==0.12.0 diff --git a/tests/components/yr/__init__.py b/tests/components/yr/__init__.py deleted file mode 100644 index d85c8ab9758..00000000000 --- a/tests/components/yr/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the yr component.""" diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py deleted file mode 100644 index b339dd9c132..00000000000 --- a/tests/components/yr/test_sensor.py +++ /dev/null @@ -1,127 +0,0 @@ -"""The tests for the Yr sensor platform.""" -from datetime import datetime - -from homeassistant.bootstrap import async_setup_component -from homeassistant.const import DEGREE, SPEED_METERS_PER_SECOND, UNIT_PERCENTAGE -import homeassistant.util.dt as dt_util - -from tests.async_mock import patch -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, legacy_patchable_time, aioclient_mock): - """Test the default setup.""" - aioclient_mock.get( - "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", - text=load_fixture("yr.no.xml"), - ) - 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): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() - - state = hass.states.get("sensor.yr_symbol") - - assert state.state == "3" - assert state.attributes.get("unit_of_measurement") is None - - -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/", - text=load_fixture("yr.no.xml"), - ) - - config = { - "platform": "yr", - "elevation": 0, - "monitored_conditions": [ - "pressure", - "windDirection", - "humidity", - "fog", - "windSpeed", - ], - } - hass.allow_pool = True - - with patch( - "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW - ), assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() - - state = hass.states.get("sensor.yr_pressure") - assert state.attributes.get("unit_of_measurement") == "hPa" - assert state.state == "1009.3" - - state = hass.states.get("sensor.yr_wind_direction") - assert state.attributes.get("unit_of_measurement") == DEGREE - assert state.state == "103.6" - - state = hass.states.get("sensor.yr_humidity") - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE - assert state.state == "55.5" - - state = hass.states.get("sensor.yr_fog") - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE - assert state.state == "0.0" - - state = hass.states.get("sensor.yr_wind_speed") - assert state.attributes.get("unit_of_measurement") == SPEED_METERS_PER_SECOND - assert state.state == "3.5" - - -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/", - text=load_fixture("yr.no.xml"), - ) - - config = { - "platform": "yr", - "elevation": 0, - "forecast": 24, - "monitored_conditions": [ - "pressure", - "windDirection", - "humidity", - "fog", - "windSpeed", - ], - } - hass.allow_pool = True - - with patch( - "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW - ), assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() - - state = hass.states.get("sensor.yr_pressure") - assert state.attributes.get("unit_of_measurement") == "hPa" - assert state.state == "1008.3" - - state = hass.states.get("sensor.yr_wind_direction") - assert state.attributes.get("unit_of_measurement") == DEGREE - assert state.state == "148.9" - - state = hass.states.get("sensor.yr_humidity") - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE - assert state.state == "77.4" - - state = hass.states.get("sensor.yr_fog") - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE - assert state.state == "0.0" - - state = hass.states.get("sensor.yr_wind_speed") - assert state.attributes.get("unit_of_measurement") == SPEED_METERS_PER_SECOND - assert state.state == "3.6" diff --git a/tests/fixtures/yr.no.xml b/tests/fixtures/yr.no.xml deleted file mode 100644 index b181fdfd85b..00000000000 --- a/tests/fixtures/yr.no.xml +++ /dev/null @@ -1,1184 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 68c0f770fee2b9e3c776ec6cc787c7e57f65655b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 25 Aug 2020 23:10:18 +0200 Subject: [PATCH 338/862] Update homeassistant base image 8.3.0 (#39245) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index a1db6ac2a54..6d8763a019f 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:8.2.1", - "armhf": "homeassistant/armhf-homeassistant-base:8.2.1", - "armv7": "homeassistant/armv7-homeassistant-base:8.2.1", - "amd64": "homeassistant/amd64-homeassistant-base:8.2.1", - "i386": "homeassistant/i386-homeassistant-base:8.2.1" + "aarch64": "homeassistant/aarch64-homeassistant-base:8.3.0", + "armhf": "homeassistant/armhf-homeassistant-base:8.3.0", + "armv7": "homeassistant/armv7-homeassistant-base:8.3.0", + "amd64": "homeassistant/amd64-homeassistant-base:8.3.0", + "i386": "homeassistant/i386-homeassistant-base:8.3.0" }, "labels": { "io.hass.type": "core" From 20398cc0a6c56b4a50fdb9bfb93bb9300da8c37a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 17:20:04 -0500 Subject: [PATCH 339/862] Subscribe to state change events only if the template has entities (#39188) --- homeassistant/helpers/event.py | 6 --- tests/components/template/test_sensor.py | 65 ++++++++++++++++++++++- tests/components/template/test_trigger.py | 25 +++++++-- tests/helpers/test_event.py | 6 --- 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 0dc4764732f..6cd6a6c2cb9 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -484,12 +484,6 @@ class _TrackTemplateResultInfo: if self._info.exception: return True - # There are no entities in the template - # to track so this template will - # re-render on EVERY state change - if not self._info.domains and not self._info.entities: - return True - return False @callback diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 3ffd9be4a64..2b4a34e8c4a 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -10,8 +10,10 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import CoreState +from homeassistant.core import CoreState, callback +from homeassistant.helpers.template import Template from homeassistant.setup import ATTR_COMPONENT, async_setup_component, setup_component +import homeassistant.util.dt as dt_util from tests.common import assert_setup_component, get_test_home_assistant @@ -694,3 +696,64 @@ async def test_unique_id(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + + +async def test_sun_renders_once_per_sensor(hass): + """Test sun change renders the template only once per sensor.""" + + now = dt_util.utcnow() + hass.states.async_set( + "sun.sun", "above_horizon", {"elevation": 45.3, "next_rising": now} + ) + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "solar_angle": { + "friendly_name": "Sun angle", + "unit_of_measurement": "degrees", + "value_template": "{{ state_attr('sun.sun', 'elevation') }}", + }, + "sunrise": { + "value_template": "{{ state_attr('sun.sun', 'next_rising') }}" + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.solar_angle").state == "45.3" + assert hass.states.get("sensor.sunrise").state == str(now) + + async_render_calls = [] + + @callback + def _record_async_render(self, *args, **kwargs): + """Catch async_render.""" + async_render_calls.append(self.template) + return "mocked" + + later = dt_util.utcnow() + + with patch.object(Template, "async_render", _record_async_render): + hass.states.async_set("sun.sun", {"elevation": 50, "next_rising": later}) + await hass.async_block_till_done() + + assert hass.states.get("sensor.solar_angle").state == "mocked" + assert hass.states.get("sensor.sunrise").state == "mocked" + + assert len(async_render_calls) == 2 + assert set(async_render_calls) == { + "{{ state_attr('sun.sun', 'elevation') }}", + "{{ state_attr('sun.sun', 'next_rising') }}", + } diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 70455fa4c22..300173fdadf 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -39,7 +39,10 @@ async def test_if_fires_on_change_bool(hass, calls): automation.DOMAIN, { automation.DOMAIN: { - "trigger": {"platform": "template", "value_template": "{{ true }}"}, + "trigger": { + "platform": "template", + "value_template": "{{ states.test.entity.state and true }}", + }, "action": {"service": "test.automation"}, } }, @@ -64,7 +67,10 @@ async def test_if_fires_on_change_str(hass, calls): automation.DOMAIN, { automation.DOMAIN: { - "trigger": {"platform": "template", "value_template": '{{ "true" }}'}, + "trigger": { + "platform": "template", + "value_template": '{{ states.test.entity.state and "true" }}', + }, "action": {"service": "test.automation"}, } }, @@ -82,7 +88,10 @@ async def test_if_fires_on_change_str_crazy(hass, calls): automation.DOMAIN, { automation.DOMAIN: { - "trigger": {"platform": "template", "value_template": '{{ "TrUE" }}'}, + "trigger": { + "platform": "template", + "value_template": '{{ states.test.entity.state and "TrUE" }}', + }, "action": {"service": "test.automation"}, } }, @@ -100,7 +109,10 @@ async def test_if_not_fires_on_change_bool(hass, calls): automation.DOMAIN, { automation.DOMAIN: { - "trigger": {"platform": "template", "value_template": "{{ false }}"}, + "trigger": { + "platform": "template", + "value_template": "{{ states.test.entity.state and false }}", + }, "action": {"service": "test.automation"}, } }, @@ -178,7 +190,10 @@ async def test_if_fires_on_two_change(hass, calls): automation.DOMAIN, { automation.DOMAIN: { - "trigger": {"platform": "template", "value_template": "{{ true }}"}, + "trigger": { + "platform": "template", + "value_template": "{{ states.test.entity.state and true }}", + }, "action": {"service": "test.automation"}, } }, diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index a9a4277bae0..80f4dc9a09f 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -542,12 +542,6 @@ async def test_track_template_error(hass, caplog): assert "lunch" not in caplog.text assert "TemplateAssertionError" not in caplog.text - hass.states.async_set("switch.not_exist", "on") - await hass.async_block_till_done() - - assert "lunch" in caplog.text - assert "TemplateAssertionError" in caplog.text - async def test_track_template_result(hass): """Test tracking template.""" From 63ebea1706f3531100822cfc8f0d013263a35cb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 17:22:10 -0500 Subject: [PATCH 340/862] Ensure the context is passed to group changes (#39221) --- homeassistant/components/group/__init__.py | 31 +++++++++++++++++- homeassistant/components/group/cover.py | 37 +++++++++++----------- homeassistant/components/group/light.py | 29 ++++++++--------- tests/components/group/test_cover.py | 2 ++ tests/components/group/test_light.py | 36 +++++++++++++++++++++ 5 files changed, 100 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 11c78a9b271..67f8096134e 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_NAME, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, + EVENT_HOMEASSISTANT_START, SERVICE_RELOAD, STATE_CLOSED, STATE_HOME, @@ -28,7 +29,7 @@ from homeassistant.const import ( STATE_UNKNOWN, STATE_UNLOCKED, ) -from homeassistant.core import callback +from homeassistant.core import CoreState, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent @@ -341,6 +342,33 @@ async def _async_process_config(hass, config, component): ) +class GroupEntity(Entity): + """Representation of a Group of entities.""" + + @property + def should_poll(self) -> bool: + """Disable polling for group.""" + return False + + async def async_added_to_hass(self) -> None: + """Register listeners.""" + assert self.hass is not None + + async def _update_at_start(_): + await self.async_update_ha_state(True) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _update_at_start) + + async def async_defer_or_update_ha_state(self) -> None: + """Only update once at start.""" + assert self.hass is not None + + if self.hass.state != CoreState.running: + return + + await self.async_update_ha_state(True) + + class Group(Entity): """Track a group of entity ids.""" @@ -545,6 +573,7 @@ class Group(Entity): if self._async_unsub_state_changed is None: return + self.async_set_context(event.context) self._async_update_group_state(event.data.get("new_state")) self.async_write_ha_state() diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 02de871cb7a..6a2111747d6 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -39,10 +39,12 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import State, callback +from homeassistant.core import State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event +from . import GroupEntity + # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs @@ -68,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])]) -class CoverGroup(CoverEntity): +class CoverGroup(GroupEntity, CoverEntity): """Representation of a CoverGroup.""" def __init__(self, name, entities): @@ -94,14 +96,13 @@ class CoverGroup(CoverEntity): KEY_POSITION: set(), } - @callback - def _update_supported_features_event(self, event): - self.update_supported_features( + async def _update_supported_features_event(self, event): + self.async_set_context(event.context) + await self.async_update_supported_features( event.data.get("entity_id"), event.data.get("new_state") ) - @callback - def update_supported_features( + async def async_update_supported_features( self, entity_id: str, new_state: Optional[State], update_state: bool = True, ) -> None: """Update dictionaries with supported features.""" @@ -111,7 +112,7 @@ class CoverGroup(CoverEntity): for values in self._tilts.values(): values.discard(entity_id) if update_state: - self.async_schedule_update_ha_state(True) + await self.async_defer_or_update_ha_state() return features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -143,17 +144,22 @@ class CoverGroup(CoverEntity): self._tilts[KEY_POSITION].discard(entity_id) if update_state: - self.async_schedule_update_ha_state(True) + await self.async_defer_or_update_ha_state() async def async_added_to_hass(self): """Register listeners.""" for entity_id in self._entities: new_state = self.hass.states.get(entity_id) - self.update_supported_features(entity_id, new_state, update_state=False) - async_track_state_change_event( - self.hass, self._entities, self._update_supported_features_event + await self.async_update_supported_features( + entity_id, new_state, update_state=False + ) + assert self.hass is not None + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entities, self._update_supported_features_event + ) ) - await self.async_update() + await super().async_added_to_hass() @property def name(self): @@ -165,11 +171,6 @@ class CoverGroup(CoverEntity): """Enable buttons even if at end position.""" return self._assumed_state - @property - def should_poll(self): - """Disable polling for cover group.""" - return False - @property def supported_features(self): """Flag supported features for the cover.""" diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 1b33a0a6e88..289bb8df3f0 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -36,12 +36,14 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import CALLBACK_TYPE, State, callback +from homeassistant.core import State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import color as color_util +from . import GroupEntity + # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs @@ -76,7 +78,7 @@ async def async_setup_platform( ) -class LightGroup(light.LightEntity): +class LightGroup(GroupEntity, light.LightEntity): """Representation of a light group.""" def __init__(self, name: str, entity_ids: List[str]) -> None: @@ -94,27 +96,22 @@ class LightGroup(light.LightEntity): self._effect_list: Optional[List[str]] = None self._effect: Optional[str] = None self._supported_features: int = 0 - self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None async def async_added_to_hass(self) -> None: """Register callbacks.""" - @callback - def async_state_changed_listener(*_): + async def async_state_changed_listener(event): """Handle child updates.""" - self.async_schedule_update_ha_state(True) + self.async_set_context(event.context) + await self.async_defer_or_update_ha_state() - assert self.hass is not None - self._async_unsub_state_changed = async_track_state_change_event( - self.hass, self._entity_ids, async_state_changed_listener + assert self.hass + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) ) - await self.async_update() - - async def async_will_remove_from_hass(self): - """Handle removal from Home Assistant.""" - if self._async_unsub_state_changed is not None: - self._async_unsub_state_changed() - self._async_unsub_state_changed = None + await super().async_added_to_hass() @property def name(self) -> str: diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 98460762389..efdbc40ee46 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -78,6 +78,8 @@ async def setup_comp(hass, config_count): with assert_setup_component(count, DOMAIN): await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 685db475b8c..8c659a5ebf6 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -47,6 +47,8 @@ async def test_default_state(hass): }, ) await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() state = hass.states.get("light.bedroom_group") assert state is not None @@ -73,6 +75,9 @@ async def test_state_reporting(hass): } }, ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set("light.test1", STATE_ON) hass.states.async_set("light.test2", STATE_UNAVAILABLE) @@ -107,6 +112,9 @@ async def test_brightness(hass): } }, ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set( "light.test1", STATE_ON, {ATTR_BRIGHTNESS: 255, ATTR_SUPPORTED_FEATURES: 1} @@ -147,6 +155,9 @@ async def test_color(hass): } }, ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set( "light.test1", STATE_ON, {ATTR_HS_COLOR: (0, 100), ATTR_SUPPORTED_FEATURES: 16} @@ -184,6 +195,9 @@ async def test_white_value(hass): } }, ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set( "light.test1", STATE_ON, {ATTR_WHITE_VALUE: 255, ATTR_SUPPORTED_FEATURES: 128} @@ -219,6 +233,9 @@ async def test_color_temp(hass): } }, ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set( "light.test1", STATE_ON, {"color_temp": 2, ATTR_SUPPORTED_FEATURES: 2} @@ -262,6 +279,8 @@ async def test_emulated_color_temp_group(hass): }, ) await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set("light.bed_light", STATE_ON, {ATTR_SUPPORTED_FEATURES: 2}) hass.states.async_set( @@ -306,6 +325,9 @@ async def test_min_max_mireds(hass): } }, ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set( "light.test1", @@ -350,6 +372,9 @@ async def test_effect_list(hass): } }, ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set( "light.test1", @@ -402,6 +427,9 @@ async def test_effect(hass): } }, ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set( "light.test1", STATE_ON, {ATTR_EFFECT: "None", ATTR_SUPPORTED_FEATURES: 6} @@ -447,6 +475,9 @@ async def test_supported_features(hass): } }, ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() hass.states.async_set("light.test1", STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() @@ -489,6 +520,8 @@ async def test_service_calls(hass): }, ) await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_ON await hass.services.async_call( @@ -559,6 +592,9 @@ async def test_invalid_service_calls(hass): await group.async_setup_platform( hass, {"entities": ["light.test1", "light.test2"]}, add_entities ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() assert add_entities.call_count == 1 grouped_light = add_entities.call_args[0][0][0] From 90842fcb846be0d989d44a5883885c9d3f73dc3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 17:25:15 -0500 Subject: [PATCH 341/862] Support reloading the universal platform (#39248) --- homeassistant/components/template/__init__.py | 51 ++------------ .../template/alarm_control_panel.py | 8 +-- .../components/template/binary_sensor.py | 8 +-- homeassistant/components/template/const.py | 12 ++++ homeassistant/components/template/cover.py | 8 +-- homeassistant/components/template/fan.py | 8 +-- homeassistant/components/template/light.py | 8 +-- homeassistant/components/template/lock.py | 8 +-- homeassistant/components/template/sensor.py | 8 +-- homeassistant/components/template/switch.py | 8 +-- homeassistant/components/template/vacuum.py | 8 +-- .../components/universal/media_player.py | 25 ++++++- .../components/universal/services.yaml | 2 + homeassistant/helpers/reload.py | 70 +++++++++++++++++++ .../components/universal/test_media_player.py | 64 +++++++++++++++++ .../helpers/reload_configuration.yaml | 14 ++++ tests/fixtures/universal/configuration.yaml | 9 +++ tests/helpers/test_reload.py | 63 +++++++++++++++++ 18 files changed, 300 insertions(+), 82 deletions(-) create mode 100644 homeassistant/helpers/reload.py create mode 100644 tests/fixtures/helpers/reload_configuration.yaml create mode 100644 tests/fixtures/universal/configuration.yaml create mode 100644 tests/helpers/test_reload.py diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index f7f40cb92f7..a0f3a6a4a65 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -2,65 +2,26 @@ import logging -from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform, entity_platform -from homeassistant.loader import async_get_integration +from homeassistant.helpers.reload import async_reload_integration_platforms -from .const import DOMAIN, EVENT_TEMPLATE_RELOADED, PLATFORM_STORAGE_KEY +from .const import DOMAIN, EVENT_TEMPLATE_RELOADED, PLATFORMS _LOGGER = logging.getLogger(__name__) -async def _async_setup_reload_service(hass): +async def async_setup_reload_service(hass): + """Create the reload service for the template domain.""" + if hass.services.has_service(DOMAIN, SERVICE_RELOAD): return async def _reload_config(call): """Reload the template platform config.""" - try: - unprocessed_conf = await conf_util.async_hass_config_yaml(hass) - except HomeAssistantError as err: - _LOGGER.error(err) - return - - for platform in hass.data[PLATFORM_STORAGE_KEY]: - - integration = await async_get_integration(hass, platform.domain) - - conf = await conf_util.async_process_component_config( - hass, unprocessed_conf, integration - ) - - if not conf: - continue - - await platform.async_reset() - - # Extract only the config for template, ignore the rest. - for p_type, p_config in config_per_platform(conf, platform.domain): - if p_type != DOMAIN: - continue - - entities = await platform.platform.async_create_entities(hass, p_config) - - await platform.async_add_entities(entities) - + await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) hass.bus.async_fire(EVENT_TEMPLATE_RELOADED, context=call.context) hass.helpers.service.async_register_admin_service( DOMAIN, SERVICE_RELOAD, _reload_config ) - - -async def async_setup_platform_reloadable(hass): - """Template platform with reloadability.""" - - await _async_setup_reload_service(hass) - - platform = entity_platform.current_platform.get() - - if platform not in hass.data.setdefault(PLATFORM_STORAGE_KEY, []): - hass.data[PLATFORM_STORAGE_KEY].append(platform) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index ac71ec74397..292c359d334 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -33,7 +33,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from . import async_setup_platform_reloadable +from . import async_setup_reload_service from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -76,7 +76,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_create_entities(hass, config): +async def _async_create_entities(hass, config): """Create Template Alarm Control Panels.""" alarm_control_panels = [] @@ -112,8 +112,8 @@ async def async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template Alarm Control Panels.""" - await async_setup_platform_reloadable(hass) - async_add_entities(await async_create_entities(hass, config)) + await async_setup_reload_service(hass) + async_add_entities(await _async_create_entities(hass, config)) class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 863bf2ab1c9..5ea04d67207 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_call_later from homeassistant.helpers.template import result_as_boolean -from . import async_setup_platform_reloadable +from . import async_setup_reload_service from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -57,7 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_create_entities(hass, config): +async def _async_create_entities(hass, config): """Create the template binary sensors.""" sensors = [] @@ -97,8 +97,8 @@ async def async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template binary sensors.""" - await async_setup_platform_reloadable(hass) - async_add_entities(await async_create_entities(hass, config)) + await async_setup_reload_service(hass) + async_add_entities(await _async_create_entities(hass, config)) class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 6d46978b86f..cf1ec8bc1c3 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -7,3 +7,15 @@ DOMAIN = "template" PLATFORM_STORAGE_KEY = "template_platforms" EVENT_TEMPLATE_RELOADED = "event_template_reloaded" + +PLATFORMS = [ + "alarm_control_panel", + "binary_sensor", + "cover", + "fan", + "light", + "lock", + "sensor", + "switch", + "vacuum", +] diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 688b116628c..828d7790ebf 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -37,7 +37,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from . import async_setup_platform_reloadable +from . import async_setup_reload_service from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -100,7 +100,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_create_entities(hass, config): +async def _async_create_entities(hass, config): """Create the Template cover.""" covers = [] @@ -152,8 +152,8 @@ async def async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template cover.""" - await async_setup_platform_reloadable(hass) - async_add_entities(await async_create_entities(hass, config)) + await async_setup_reload_service(hass) + async_add_entities(await _async_create_entities(hass, config)) class CoverTemplate(TemplateEntity, CoverEntity): diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 747d8b522a5..b7136787948 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -34,7 +34,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from . import async_setup_platform_reloadable +from . import async_setup_reload_service from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -81,7 +81,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( ) -async def async_create_entities(hass, config): +async def _async_create_entities(hass, config): """Create the Template Fans.""" fans = [] @@ -129,8 +129,8 @@ async def async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template fans.""" - await async_setup_platform_reloadable(hass) - async_add_entities(await async_create_entities(hass, config)) + await async_setup_reload_service(hass) + async_add_entities(await _async_create_entities(hass, config)) class TemplateFan(TemplateEntity, FanEntity): diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index c066a5d66d0..8fa2ae5f632 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -33,7 +33,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from . import async_setup_platform_reloadable +from . import async_setup_reload_service from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -78,7 +78,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_create_entities(hass, config): +async def _async_create_entities(hass, config): """Create the Template Lights.""" lights = [] @@ -135,8 +135,8 @@ async def async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lights.""" - await async_setup_platform_reloadable(hass) - async_add_entities(await async_create_entities(hass, config)) + await async_setup_reload_service(hass) + async_add_entities(await _async_create_entities(hass, config)) class LightTemplate(TemplateEntity, LightEntity): diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 3e91e7b05c0..74197e6eb6d 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -17,7 +17,7 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -from . import async_setup_platform_reloadable +from . import async_setup_reload_service from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_create_entities(hass, config): +async def _async_create_entities(hass, config): """Create the Template lock.""" device = config.get(CONF_NAME) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -65,8 +65,8 @@ async def async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lock.""" - await async_setup_platform_reloadable(hass) - async_add_entities(await async_create_entities(hass, config)) + await async_setup_reload_service(hass) + async_add_entities(await _async_create_entities(hass, config)) class TemplateLock(TemplateEntity, LockEntity): diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index da99ac40ed2..f6844364928 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -26,7 +26,7 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id -from . import async_setup_platform_reloadable +from . import async_setup_reload_service from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -57,7 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_create_entities(hass, config): +async def _async_create_entities(hass, config): """Create the template sensors.""" sensors = [] @@ -97,8 +97,8 @@ async def async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template sensors.""" - await async_setup_platform_reloadable(hass) - async_add_entities(await async_create_entities(hass, config)) + await async_setup_reload_service(hass) + async_add_entities(await _async_create_entities(hass, config)) class SensorTemplate(TemplateEntity, Entity): diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 995f12584e6..7ef540144d8 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import Script -from . import async_setup_platform_reloadable +from . import async_setup_reload_service from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -55,7 +55,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_create_entities(hass, config): +async def _async_create_entities(hass, config): """Create the Template switches.""" switches = [] @@ -90,8 +90,8 @@ async def async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template switches.""" - await async_setup_platform_reloadable(hass) - async_add_entities(await async_create_entities(hass, config)) + await async_setup_reload_service(hass) + async_add_entities(await _async_create_entities(hass, config)) class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 3fd2c0f6ad1..b6b6669f7b9 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -43,7 +43,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script -from . import async_setup_platform_reloadable +from . import async_setup_reload_service from .const import CONF_AVAILABILITY_TEMPLATE from .template_entity import TemplateEntity @@ -93,7 +93,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( ) -async def async_create_entities(hass, config): +async def _async_create_entities(hass, config): """Create the Template Vacuums.""" vacuums = [] @@ -145,8 +145,8 @@ async def async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template vacuums.""" - await async_setup_platform_reloadable(hass) - async_add_entities(await async_create_entities(hass, config)) + await async_setup_reload_service(hass) + async_add_entities(await _async_create_entities(hass, config)) class TemplateVacuum(TemplateEntity, StateVacuumEntity): diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index bff5ad1542b..aaf4464452b 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -56,6 +56,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, + SERVICE_RELOAD, SERVICE_SHUFFLE_SET, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -71,6 +72,7 @@ from homeassistant.const import ( from homeassistant.core import EVENT_HOMEASSISTANT_START, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.service import async_call_from_config _LOGGER = logging.getLogger(__name__) @@ -102,9 +104,31 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( extra=vol.REMOVE_EXTRA, ) +EVENT_UNIVERSAL_RELOADED = "event_universal_reloaded" + + +async def async_setup_reload_service(hass): + """Create the reload service for the universal domain.""" + + if hass.services.has_service("universal", SERVICE_RELOAD): + return + + async def _reload_config(call): + """Reload the template universal config.""" + + await async_reload_integration_platforms(hass, "universal", ["media_player"]) + hass.bus.async_fire(EVENT_UNIVERSAL_RELOADED, context=call.context) + + hass.helpers.service.async_register_admin_service( + "universal", SERVICE_RELOAD, _reload_config + ) + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the universal media players.""" + + await async_setup_reload_service(hass) + player = UniversalMediaPlayer( hass, config.get(CONF_NAME), @@ -157,7 +181,6 @@ class UniversalMediaPlayer(MediaPlayerEntity): result = self.hass.helpers.event.async_track_template_result( self._state_template, _async_on_template_update ) - self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, callback(lambda _: result.async_refresh()) ) diff --git a/homeassistant/components/universal/services.yaml b/homeassistant/components/universal/services.yaml index e69de29bb2d..ed8f550275e 100644 --- a/homeassistant/components/universal/services.yaml +++ b/homeassistant/components/universal/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all universal entities. diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py new file mode 100644 index 00000000000..3922f3710e2 --- /dev/null +++ b/homeassistant/helpers/reload.py @@ -0,0 +1,70 @@ +"""Class to reload platforms.""" + +import logging +from typing import Optional + +from homeassistant import config as conf_util +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.entity_platform import DATA_ENTITY_PLATFORM, EntityPlatform +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import async_get_integration + +_LOGGER = logging.getLogger(__name__) + + +async def async_reload_integration_platforms( + hass: HomeAssistantType, integration_name: str, integration_platforms: str +) -> None: + """Reload an integration's platforms. + + The platform must support being re-setup. + + This functionality is only intended to be used for integrations that process + Home Assistant data and make this available to other integrations. + + Examples are template, stats, derivative, utility meter. + """ + try: + unprocessed_conf = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + for integration_platform in integration_platforms: + platform = async_get_platform(hass, integration_name, integration_platform) + + if not platform: + continue + + integration = await async_get_integration(hass, integration_platform) + + conf = await conf_util.async_process_component_config( + hass, unprocessed_conf, integration + ) + + if not conf: + continue + + await platform.async_reset() + + # Extract only the config for template, ignore the rest. + for p_type, p_config in config_per_platform(conf, integration_platform): + if p_type != integration_name: + continue + + await platform.async_setup(p_config) # type: ignore + + +@callback +def async_get_platform( + hass: HomeAssistantType, integration_name: str, integration_platform_name: str +) -> Optional[EntityPlatform]: + """Find an existing platform.""" + for integration_platform in hass.data[DATA_ENTITY_PLATFORM][integration_name]: + if integration_platform.domain == integration_platform_name: + platform: EntityPlatform = integration_platform + return platform + + return None diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index af2132e8f69..8f274fcc9c9 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -1,10 +1,13 @@ """The tests for the Universal Media player platform.""" import asyncio from copy import copy +from os import path import unittest +from asynctest.mock import patch from voluptuous.error import MultipleInvalid +from homeassistant import config as hass_config import homeassistant.components.input_number as input_number import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player @@ -812,3 +815,64 @@ async def test_master_state_with_template(hass): await hass.async_block_till_done() hass.states.get("media_player.tv").state == STATE_OFF + + +async def test_reload(hass): + """Test the state_template option.""" + hass.states.async_set("input_boolean.test", STATE_OFF) + hass.states.async_set("media_player.mock1", STATE_OFF) + + templ = ( + '{% if states.input_boolean.test.state == "off" %}on' + "{% else %}{{ states.media_player.mock1.state }}{% endif %}" + ) + + await async_setup_component( + hass, + "media_player", + { + "media_player": { + "platform": "universal", + "name": "tv", + "state_template": templ, + } + }, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + await hass.async_start() + + await hass.async_block_till_done() + hass.states.get("media_player.tv").state == STATE_ON + + hass.states.async_set("input_boolean.test", STATE_ON) + await hass.async_block_till_done() + + hass.states.get("media_player.tv").state == STATE_OFF + + hass.states.async_set("media_player.master_bedroom_2", STATE_OFF) + hass.states.async_set( + "remote.alexander_master_bedroom", + STATE_ON, + {"activity_list": ["act1", "act2"], "current_activity": "act2"}, + ) + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "universal/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "universal", universal.SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 5 + + assert hass.states.get("media_player.tv") is None + assert hass.states.get("media_player.master_bed_tv").state == "on" + assert hass.states.get("media_player.master_bed_tv").attributes["source"] == "act2" + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/helpers/reload_configuration.yaml b/tests/fixtures/helpers/reload_configuration.yaml new file mode 100644 index 00000000000..f7c628bec5a --- /dev/null +++ b/tests/fixtures/helpers/reload_configuration.yaml @@ -0,0 +1,14 @@ +test_domain: + - platform: test_platform + sensors: + combined_sensor_energy_usage: + friendly_name: Combined Sense Energy Usage + unit_of_measurement: kW + value_template: '{{ ((states(''sensor.energy_usage'') | float) + (states(''sensor.energy_usage_2'') + | float)) / 1000 }}' + watching_tv_in_master_bedroom: + friendly_name: Watching TV in Master Bedroom + value_template: '{% if state_attr("remote.alexander_master_bedroom","current_activity") + == "Watch TV" or state_attr("remote.alexander_master_bedroom","current_activity") + == "Watch Apple TV" %}on{% else %}off{% endif %}' + diff --git a/tests/fixtures/universal/configuration.yaml b/tests/fixtures/universal/configuration.yaml new file mode 100644 index 00000000000..c3e445615f1 --- /dev/null +++ b/tests/fixtures/universal/configuration.yaml @@ -0,0 +1,9 @@ +media_player: + - platform: universal + name: Master Bed TV + children: + - media_player.master_bedroom_2 + attributes: + state: remote.alexander_master_bedroom + source_list: remote.alexander_master_bedroom|activity_list + source: remote.alexander_master_bedroom|current_activity diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py new file mode 100644 index 00000000000..d9067b4b35f --- /dev/null +++ b/tests/helpers/test_reload.py @@ -0,0 +1,63 @@ +"""Tests for the reload helper.""" +import logging +from os import path + +from homeassistant import config +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.reload import ( + async_get_platform, + async_reload_integration_platforms, +) + +from tests.async_mock import Mock, patch +from tests.common import ( + MockModule, + MockPlatform, + mock_entity_platform, + mock_integration, +) + +_LOGGER = logging.getLogger(__name__) +DOMAIN = "test_domain" +PLATFORM = "test_platform" + + +async def test_reload_platform(hass): + """Test the polling of only updated entities.""" + component_setup = Mock(return_value=True) + + setup_called = [] + + async def setup_platform(*args): + setup_called.append(args) + + mock_integration(hass, MockModule(DOMAIN, setup=component_setup)) + mock_integration(hass, MockModule(PLATFORM, dependencies=[DOMAIN])) + + mock_platform = MockPlatform(async_setup_platform=setup_platform) + mock_entity_platform(hass, f"{DOMAIN}.{PLATFORM}", mock_platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_setup({DOMAIN: {"platform": PLATFORM, "sensors": None}}) + await hass.async_block_till_done() + assert component_setup.called + + assert f"{DOMAIN}.{PLATFORM}" in hass.config.components + assert len(setup_called) == 1 + + platform = async_get_platform(hass, PLATFORM, DOMAIN) + assert platform.platform_name == PLATFORM + assert platform.domain == DOMAIN + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "helpers/reload_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + await async_reload_integration_platforms(hass, PLATFORM, [DOMAIN]) + + assert len(setup_called) == 2 + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(__file__)) From c87e03ee6f1dd25107c2663ca60a51badc606b40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 17:33:08 -0500 Subject: [PATCH 342/862] Ensure template tracking can recover after the template generates an exception (#39256) --- homeassistant/helpers/event.py | 9 +++++++++ tests/helpers/test_event.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 6cd6a6c2cb9..ba0b07a207e 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -562,6 +562,11 @@ class _TrackTemplateResultInfo: entities = set(self._info.entities) for entity_id in self.hass.states.async_entity_ids(self._info.domains): entities.add(entity_id) + + # Entities has changed to none + if not entities: + return + self._entities_listener = async_track_state_change_event( self.hass, entities, self._refresh ) @@ -570,6 +575,10 @@ class _TrackTemplateResultInfo: def _setup_domains_listener(self) -> None: assert self._info + # Domains has changed to none + if not self._info.domains: + return + self._domains_listener = async_track_state_added_domain( self.hass, self._info.domains, self._refresh ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 80f4dc9a09f..6fb422e03e7 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -543,6 +543,33 @@ async def test_track_template_error(hass, caplog): assert "TemplateAssertionError" not in caplog.text +async def test_track_template_error_can_recover(hass, caplog): + """Test tracking template with error.""" + hass.states.async_set("switch.data_system", "cow", {"opmode": 0}) + template_error = Template( + "{{ states.sensor.data_system.attributes['opmode'] == '0' }}", hass + ) + error_calls = [] + + @ha.callback + def error_callback(entity_id, old_state, new_state): + error_calls.append((entity_id, old_state, new_state)) + + async_track_template(hass, template_error, error_callback) + await hass.async_block_till_done() + assert not error_calls + + hass.states.async_remove("switch.data_system") + + assert "UndefinedError" in caplog.text + + hass.states.async_set("switch.data_system", "cow", {"opmode": 0}) + + caplog.clear() + + assert "UndefinedError" not in caplog.text + + async def test_track_template_result(hass): """Test tracking template.""" specific_runs = [] From 11f121b0083fbe97a23312ce06838c26da766045 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Wed, 26 Aug 2020 00:37:53 +0200 Subject: [PATCH 343/862] Implement local discovery of Smappee series-2 devices and improvements (#38728) * prepare local api support for Smappee2-series * Series-2 devices are now supported * remove switch scan_interval --- homeassistant/components/smappee/config_flow.py | 15 ++++++++++++--- homeassistant/components/smappee/const.py | 2 ++ homeassistant/components/smappee/manifest.json | 2 +- homeassistant/components/smappee/switch.py | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smappee/test_config_flow.py | 2 +- 7 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/smappee/config_flow.py b/homeassistant/components/smappee/config_flow.py index 32f74aa9736..26a47815f34 100644 --- a/homeassistant/components/smappee/config_flow.py +++ b/homeassistant/components/smappee/config_flow.py @@ -8,7 +8,14 @@ from homeassistant.const import CONF_HOST, CONF_IP_ADDRESS from homeassistant.helpers import config_entry_oauth2_flow from . import api -from .const import CONF_HOSTNAME, CONF_SERIALNUMBER, DOMAIN, ENV_CLOUD, ENV_LOCAL +from .const import ( + CONF_HOSTNAME, + CONF_SERIALNUMBER, + DOMAIN, + ENV_CLOUD, + ENV_LOCAL, + SUPPORTED_LOCAL_DEVICES, +) _LOGGER = logging.getLogger(__name__) @@ -35,7 +42,7 @@ class SmappeeFlowHandler( async def async_step_zeroconf(self, discovery_info): """Handle zeroconf discovery.""" - if not discovery_info[CONF_HOSTNAME].startswith("Smappee1"): + if not discovery_info[CONF_HOSTNAME].startswith(SUPPORTED_LOCAL_DEVICES): # We currently only support Energy and Solar models (legacy) return self.async_abort(reason="invalid_mdns") @@ -152,7 +159,9 @@ class SmappeeFlowHandler( if config_item["key"] == "mdnsHostName": serial_number = config_item["value"] - if serial_number is None or not serial_number.startswith("Smappee1"): + if serial_number is None or not serial_number.startswith( + SUPPORTED_LOCAL_DEVICES + ): # We currently only support Energy and Solar models (legacy) return self.async_abort(reason="invalid_mdns") diff --git a/homeassistant/components/smappee/const.py b/homeassistant/components/smappee/const.py index 531327b8369..2c69f1ccb96 100644 --- a/homeassistant/components/smappee/const.py +++ b/homeassistant/components/smappee/const.py @@ -14,6 +14,8 @@ ENV_LOCAL = "local" SMAPPEE_PLATFORMS = ["binary_sensor", "sensor", "switch"] +SUPPORTED_LOCAL_DEVICES = ("Smappee1", "Smappee2") + MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=20) AUTHORIZE_URL = { diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index fe0b0de281c..1d918c06cc0 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.2.9" + "pysmappee==0.2.10" ], "codeowners": [ "@bsmappee" diff --git a/homeassistant/components/smappee/switch.py b/homeassistant/components/smappee/switch.py index a845386e71c..4d158df852a 100644 --- a/homeassistant/components/smappee/switch.py +++ b/homeassistant/components/smappee/switch.py @@ -1,5 +1,4 @@ """Support for interacting with Smappee Comport Plugs, Switches and Output Modules.""" -from datetime import timedelta import logging from homeassistant.components.switch import SwitchEntity @@ -10,7 +9,6 @@ _LOGGER = logging.getLogger(__name__) SWITCH_PREFIX = "Switch" ICON = "mdi:toggle-switch" -SCAN_INTERVAL = timedelta(seconds=5) async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/requirements_all.txt b/requirements_all.txt index d2f43817c20..e543e325592 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1633,7 +1633,7 @@ pyskyqhub==0.1.1 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.9 +pysmappee==0.2.10 # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1815470aa7..3b5d1765455 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -771,7 +771,7 @@ pysignalclirestapi==0.3.4 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.9 +pysmappee==0.2.10 # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 38f3ac480ef..92e5ebfb7e9 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -119,7 +119,7 @@ async def test_full_user_wrong_mdns(hass): """Test we abort user flow if unsupported mDNS name got resolved.""" with patch("pysmappee.api.SmappeeLocalApi.logon", return_value={}), patch( "pysmappee.api.SmappeeLocalApi.load_advanced_config", - return_value=[{"key": "mdnsHostName", "value": "Smappee2006000212"}], + return_value=[{"key": "mdnsHostName", "value": "Smappee5010000001"}], ), patch( "pysmappee.api.SmappeeLocalApi.load_command_control_config", return_value=[] ), patch( From e109b04efe01cdd341d0c22b274a64f927e989d3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 17:59:22 -0500 Subject: [PATCH 344/862] Add api to reload config entries (#39068) --- .../components/config/config_entries.py | 27 +++++++++- homeassistant/config_entries.py | 10 ++-- .../components/config/test_config_entries.py | 50 ++++++++++++++++++- tests/test_config_entries.py | 50 ++++++++++++++++++- 4 files changed, 129 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 32934d4e970..f67bfb98641 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.const import HTTP_NOT_FOUND +from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND from homeassistant.core import callback from homeassistant.exceptions import Unauthorized import homeassistant.helpers.config_validation as cv @@ -22,6 +22,7 @@ async def async_setup(hass): """Enable the Home Assistant views.""" hass.http.register_view(ConfigManagerEntryIndexView) hass.http.register_view(ConfigManagerEntryResourceView) + hass.http.register_view(ConfigManagerEntryResourceReloadView) hass.http.register_view(ConfigManagerFlowIndexView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) @@ -92,6 +93,29 @@ class ConfigManagerEntryResourceView(HomeAssistantView): return self.json(result) +class ConfigManagerEntryResourceReloadView(HomeAssistantView): + """View to reload a config entry.""" + + url = "/api/config/config_entries/entry/{entry_id}/reload" + name = "api:config:config_entries:entry:resource:reload" + + async def post(self, request, entry_id): + """Reload a config entry.""" + if not request["hass_user"].is_admin: + raise Unauthorized(config_entry_id=entry_id, permission="remove") + + hass = request.app["hass"] + + try: + result = await hass.config_entries.async_reload(entry_id) + except config_entries.OperationNotAllowed: + return self.json_message("Entry cannot be reloaded", HTTP_FORBIDDEN) + except config_entries.UnknownEntry: + return self.json_message("Invalid entry specified", HTTP_NOT_FOUND) + + return self.json({"require_restart": not result}) + + class ConfigManagerFlowIndexView(FlowManagerIndexView): """View to create config flows.""" @@ -345,4 +369,5 @@ def entry_json(entry: config_entries.ConfigEntry) -> dict: "state": entry.state, "connection_class": entry.connection_class, "supports_options": supports_options, + "supports_unload": entry.supports_unload, } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 04bdbf236a5..01eb63fe05f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -110,6 +110,7 @@ class ConfigEntry: "data", "options", "unique_id", + "supports_unload", "system_options", "source", "connection_class", @@ -167,6 +168,9 @@ class ConfigEntry: # Unique ID of this entry. self.unique_id = unique_id + # Supports unload + self.supports_unload = False + # Listeners to call on update self.update_listeners: List[weakref.ReferenceType[UpdateListenerType]] = [] @@ -187,6 +191,8 @@ class ConfigEntry: if integration is None: integration = await loader.async_get_integration(hass, self.domain) + self.supports_unload = await support_entry_unload(hass, self.domain) + try: component = integration.get_component() except ImportError as err: @@ -1116,9 +1122,7 @@ class EntityRegistryDisabledHandler: ) assert config_entry is not None - if config_entry.entry_id not in self.changed and await support_entry_unload( - self.hass, config_entry.domain - ): + if config_entry.entry_id not in self.changed and config_entry.supports_unload: self.changed.add(config_entry.entry_id) if not self.changed: diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index e5da27818fc..9bd8875add0 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -53,12 +53,14 @@ async def test_get_entries(hass, client): "comp2", "Comp 2", lambda: None, core_ce.CONN_CLASS_ASSUMED ) - MockConfigEntry( + entry = MockConfigEntry( domain="comp1", title="Test 1", source="bla", connection_class=core_ce.CONN_CLASS_LOCAL_POLL, - ).add_to_hass(hass) + ) + entry.supports_unload = True + entry.add_to_hass(hass) MockConfigEntry( domain="comp2", title="Test 2", @@ -80,6 +82,7 @@ async def test_get_entries(hass, client): "state": "not_loaded", "connection_class": "local_poll", "supports_options": True, + "supports_unload": True, }, { "domain": "comp2", @@ -88,6 +91,7 @@ async def test_get_entries(hass, client): "state": "loaded", "connection_class": "assumed", "supports_options": False, + "supports_unload": False, }, ] @@ -103,6 +107,25 @@ async def test_remove_entry(hass, client): assert len(hass.config_entries.async_entries()) == 0 +async def test_reload_entry(hass, client): + """Test reloading an entry via the API.""" + entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry.add_to_hass(hass) + resp = await client.post( + f"/api/config/config_entries/entry/{entry.entry_id}/reload" + ) + assert resp.status == 200 + data = await resp.json() + assert data == {"require_restart": True} + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_reload_invalid_entry(hass, client): + """Test reloading an invalid entry via the API.""" + resp = await client.post("/api/config/config_entries/entry/invalid/reload") + assert resp.status == 404 + + async def test_remove_entry_unauth(hass, client, hass_admin_user): """Test removing an entry via the API.""" hass_admin_user.groups = [] @@ -113,6 +136,29 @@ async def test_remove_entry_unauth(hass, client, hass_admin_user): assert len(hass.config_entries.async_entries()) == 1 +async def test_reload_entry_unauth(hass, client, hass_admin_user): + """Test reloading an entry via the API.""" + hass_admin_user.groups = [] + entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry.add_to_hass(hass) + resp = await client.post( + f"/api/config/config_entries/entry/{entry.entry_id}/reload" + ) + assert resp.status == 401 + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_reload_entry_in_failed_state(hass, client, hass_admin_user): + """Test reloading an entry via the API that has already failed to unload.""" + entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_FAILED_UNLOAD) + entry.add_to_hass(hass) + resp = await client.post( + f"/api/config/config_entries/entry/{entry.entry_id}/reload" + ) + assert resp.status == 403 + assert len(hass.config_entries.async_entries()) == 1 + + async def test_available_flows(hass, client): """Test querying the available flows.""" with patch.object(config_flows, "FLOWS", ["hello", "world"]): diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 988e67718ef..816530befea 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -53,6 +53,7 @@ async def test_call_setup_entry(hass): """Test we call .setup_entry.""" entry = MockConfigEntry(domain="comp") entry.add_to_hass(hass) + assert not entry.supports_unload mock_setup_entry = AsyncMock(return_value=True) mock_migrate_entry = AsyncMock(return_value=True) @@ -67,16 +68,49 @@ async def test_call_setup_entry(hass): ) mock_entity_platform(hass, "config_flow.comp", None) - result = await async_setup_component(hass, "comp", {}) + with patch("homeassistant.config_entries.support_entry_unload", return_value=True): + result = await async_setup_component(hass, "comp", {}) + await hass.async_block_till_done() assert result assert len(mock_migrate_entry.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 1 assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.supports_unload + + +async def test_call_setup_entry_without_reload_support(hass): + """Test we call .setup_entry and the does not support unloading.""" + entry = MockConfigEntry(domain="comp") + entry.add_to_hass(hass) + assert not entry.supports_unload + + mock_setup_entry = AsyncMock(return_value=True) + mock_migrate_entry = AsyncMock(return_value=True) + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + with patch("homeassistant.config_entries.support_entry_unload", return_value=False): + result = await async_setup_component(hass, "comp", {}) + await hass.async_block_till_done() + assert result + assert len(mock_migrate_entry.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert not entry.supports_unload async def test_call_async_migrate_entry(hass): """Test we call .async_migrate_entry when version mismatch.""" entry = MockConfigEntry(domain="comp") + assert not entry.supports_unload entry.version = 2 entry.add_to_hass(hass) @@ -93,11 +127,14 @@ async def test_call_async_migrate_entry(hass): ) mock_entity_platform(hass, "config_flow.comp", None) - result = await async_setup_component(hass, "comp", {}) + with patch("homeassistant.config_entries.support_entry_unload", return_value=True): + result = await async_setup_component(hass, "comp", {}) + await hass.async_block_till_done() assert result assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.supports_unload async def test_call_async_migrate_entry_failure_false(hass): @@ -105,6 +142,7 @@ async def test_call_async_migrate_entry_failure_false(hass): entry = MockConfigEntry(domain="comp") entry.version = 2 entry.add_to_hass(hass) + assert not entry.supports_unload mock_migrate_entry = AsyncMock(return_value=False) mock_setup_entry = AsyncMock(return_value=True) @@ -124,6 +162,7 @@ async def test_call_async_migrate_entry_failure_false(hass): assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert not entry.supports_unload async def test_call_async_migrate_entry_failure_exception(hass): @@ -131,6 +170,7 @@ async def test_call_async_migrate_entry_failure_exception(hass): entry = MockConfigEntry(domain="comp") entry.version = 2 entry.add_to_hass(hass) + assert not entry.supports_unload mock_migrate_entry = AsyncMock(side_effect=Exception) mock_setup_entry = AsyncMock(return_value=True) @@ -150,6 +190,7 @@ async def test_call_async_migrate_entry_failure_exception(hass): assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert not entry.supports_unload async def test_call_async_migrate_entry_failure_not_bool(hass): @@ -157,6 +198,7 @@ async def test_call_async_migrate_entry_failure_not_bool(hass): entry = MockConfigEntry(domain="comp") entry.version = 2 entry.add_to_hass(hass) + assert not entry.supports_unload mock_migrate_entry = AsyncMock(return_value=None) mock_setup_entry = AsyncMock(return_value=True) @@ -176,6 +218,7 @@ async def test_call_async_migrate_entry_failure_not_bool(hass): assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert not entry.supports_unload async def test_call_async_migrate_entry_failure_not_supported(hass): @@ -183,6 +226,7 @@ async def test_call_async_migrate_entry_failure_not_supported(hass): entry = MockConfigEntry(domain="comp") entry.version = 2 entry.add_to_hass(hass) + assert not entry.supports_unload mock_setup_entry = AsyncMock(return_value=True) @@ -193,6 +237,7 @@ async def test_call_async_migrate_entry_failure_not_supported(hass): assert result assert len(mock_setup_entry.mock_calls) == 0 assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert not entry.supports_unload async def test_remove_entry(hass, manager): @@ -991,6 +1036,7 @@ async def test_reload_entry_entity_registry_works(hass): config_entry = MockConfigEntry( domain="comp", state=config_entries.ENTRY_STATE_LOADED ) + config_entry.supports_unload = True config_entry.add_to_hass(hass) mock_setup_entry = AsyncMock(return_value=True) mock_unload_entry = AsyncMock(return_value=True) From 810df38f0d9b322380e7044e74907477a9d7fc37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 18:13:43 -0500 Subject: [PATCH 345/862] Add the ability to reload light/cover groups from yaml (#39250) * Add the ability to reload light/cover groups from yaml Update previous usage to reduce code duplication. * Fix conflict from rebase --- homeassistant/components/group/__init__.py | 5 ++ .../template/alarm_control_panel.py | 5 +- .../components/template/binary_sensor.py | 6 +-- homeassistant/components/template/cover.py | 6 +-- homeassistant/components/template/fan.py | 6 +-- homeassistant/components/template/light.py | 6 +-- homeassistant/components/template/lock.py | 6 +-- homeassistant/components/template/sensor.py | 6 +-- homeassistant/components/template/switch.py | 6 +-- homeassistant/components/template/vacuum.py | 10 ++-- .../components/universal/media_player.py | 24 +-------- homeassistant/helpers/reload.py | 37 +++++++++++-- tests/components/group/test_light.py | 52 ++++++++++++++++++- .../components/universal/test_media_player.py | 5 +- tests/fixtures/group/configuration.yaml | 11 ++++ tests/helpers/test_reload.py | 40 ++++++++++++++ 16 files changed, 175 insertions(+), 56 deletions(-) create mode 100644 tests/fixtures/group/configuration.yaml diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 67f8096134e..1dc9a187030 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -34,6 +34,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass @@ -57,6 +58,8 @@ ATTR_ALL = "all" SERVICE_SET = "set" SERVICE_REMOVE = "remove" +PLATFORMS = ["light", "cover"] + _LOGGER = logging.getLogger(__name__) @@ -219,6 +222,8 @@ async def async_setup(hass, config): await component.async_add_entities(auto) + await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) + hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) ) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 292c359d334..e37c7e2982e 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -31,9 +31,10 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service +from .const import DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -112,7 +113,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template Alarm Control Panels.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 5ea04d67207..a15226e7e64 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -24,10 +24,10 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.template import result_as_boolean -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -97,7 +97,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template binary sensors.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 828d7790ebf..e26c0fca8f4 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -35,10 +35,10 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -152,7 +152,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template cover.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index b7136787948..01cf22c4aab 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -32,10 +32,10 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -129,7 +129,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template fans.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 8fa2ae5f632..a69c148ea8f 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -31,10 +31,10 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -135,7 +135,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lights.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 74197e6eb6d..b917430a6ff 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -15,10 +15,10 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -65,7 +65,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template lock.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index f6844364928..01c13af0ef2 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -25,9 +25,9 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" @@ -97,7 +97,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template sensors.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 7ef540144d8..c612d3307da 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -23,11 +23,11 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -90,7 +90,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template switches.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index b6b6669f7b9..6375995ed7d 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_FAN_SPEED, - DOMAIN, + DOMAIN as VACUUM_DOMAIN, SERVICE_CLEAN_SPOT, SERVICE_LOCATE, SERVICE_PAUSE, @@ -41,10 +41,10 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.script import Script -from . import async_setup_reload_service -from .const import CONF_AVAILABILITY_TEMPLATE +from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS from .template_entity import TemplateEntity _LOGGER = logging.getLogger(__name__) @@ -55,7 +55,7 @@ CONF_FAN_SPEED_LIST = "fan_speeds" CONF_FAN_SPEED_TEMPLATE = "fan_speed_template" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" -ENTITY_ID_FORMAT = DOMAIN + ".{}" +ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}" _VALID_STATES = [ STATE_CLEANING, STATE_DOCKED, @@ -145,7 +145,7 @@ async def _async_create_entities(hass, config): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template vacuums.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) async_add_entities(await _async_create_entities(hass, config)) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index aaf4464452b..7d1ac9953b6 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -56,7 +56,6 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, - SERVICE_RELOAD, SERVICE_SHUFFLE_SET, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -72,7 +71,7 @@ from homeassistant.const import ( from homeassistant.core import EVENT_HOMEASSISTANT_START, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.service import async_call_from_config _LOGGER = logging.getLogger(__name__) @@ -104,30 +103,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( extra=vol.REMOVE_EXTRA, ) -EVENT_UNIVERSAL_RELOADED = "event_universal_reloaded" - - -async def async_setup_reload_service(hass): - """Create the reload service for the universal domain.""" - - if hass.services.has_service("universal", SERVICE_RELOAD): - return - - async def _reload_config(call): - """Reload the template universal config.""" - - await async_reload_integration_platforms(hass, "universal", ["media_player"]) - hass.bus.async_fire(EVENT_UNIVERSAL_RELOADED, context=call.context) - - hass.helpers.service.async_register_admin_service( - "universal", SERVICE_RELOAD, _reload_config - ) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the universal media players.""" - await async_setup_reload_service(hass) + await async_setup_reload_service(hass, "universal", ["media_player"]) player = UniversalMediaPlayer( hass, diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 3922f3710e2..73d78501578 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -1,10 +1,12 @@ """Class to reload platforms.""" +import asyncio import logging -from typing import Optional +from typing import Iterable, Optional from homeassistant import config as conf_util -from homeassistant.core import callback +from homeassistant.const import SERVICE_RELOAD +from homeassistant.core import Event, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity_platform import DATA_ENTITY_PLATFORM, EntityPlatform @@ -15,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) async def async_reload_integration_platforms( - hass: HomeAssistantType, integration_name: str, integration_platforms: str + hass: HomeAssistantType, integration_name: str, integration_platforms: Iterable ) -> None: """Reload an integration's platforms. @@ -68,3 +70,32 @@ def async_get_platform( return platform return None + + +async def async_setup_reload_service( + hass: HomeAssistantType, domain: str, platforms: Iterable +) -> None: + """Create the reload service for the domain.""" + + if hass.services.has_service(domain, SERVICE_RELOAD): + return + + async def _reload_config(call: Event) -> None: + """Reload the platforms.""" + + await async_reload_integration_platforms(hass, domain, platforms) + hass.bus.async_fire(f"event_{domain}_reloaded", context=call.context) + + hass.helpers.service.async_register_admin_service( + domain, SERVICE_RELOAD, _reload_config + ) + + +def setup_reload_service( + hass: HomeAssistantType, domain: str, platforms: Iterable +) -> None: + """Sync version of async_setup_reload_service.""" + + asyncio.run_coroutine_threadsafe( + async_setup_reload_service(hass, domain, platforms), hass.loop, + ).result() diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 8c659a5ebf6..5ca008ba91f 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,5 +1,10 @@ """The tests for the Group Light platform.""" -from homeassistant.components.group import DOMAIN +from os import path + +from asynctest.mock import patch + +from homeassistant import config as hass_config +from homeassistant.components.group import DOMAIN, SERVICE_RELOAD import homeassistant.components.group.light as group from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -632,3 +637,48 @@ async def test_invalid_service_calls(hass): mock_call.assert_called_once_with( LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=None ) + + +async def test_reload(hass): + """Test the ability to reload lights.""" + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "demo"}, + { + "platform": DOMAIN, + "entities": [ + "light.bed_light", + "light.ceiling_lights", + "light.kitchen_lights", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + + await hass.async_block_till_done() + await hass.async_start() + + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "group/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("light.light_group") is None + assert hass.states.get("light.master_hall_lights_g") is not None + assert hass.states.get("light.outside_patio_lights_g") is not None + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 8f274fcc9c9..52fbab19539 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -14,6 +14,7 @@ import homeassistant.components.media_player as media_player import homeassistant.components.switch as switch import homeassistant.components.universal.media_player as universal from homeassistant.const import ( + SERVICE_RELOAD, STATE_OFF, STATE_ON, STATE_PAUSED, @@ -818,7 +819,7 @@ async def test_master_state_with_template(hass): async def test_reload(hass): - """Test the state_template option.""" + """Test reloading the media player from yaml.""" hass.states.async_set("input_boolean.test", STATE_OFF) hass.states.async_set("media_player.mock1", STATE_OFF) @@ -863,7 +864,7 @@ async def test_reload(hass): ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "universal", universal.SERVICE_RELOAD, {}, blocking=True, + "universal", SERVICE_RELOAD, {}, blocking=True, ) await hass.async_block_till_done() diff --git a/tests/fixtures/group/configuration.yaml b/tests/fixtures/group/configuration.yaml new file mode 100644 index 00000000000..9047024e3de --- /dev/null +++ b/tests/fixtures/group/configuration.yaml @@ -0,0 +1,11 @@ +light: + - platform: group + name: Master Hall Lights G + entities: + - light.master_hall_lights + - light.master_hall_lights_2 + - platform: group + name: Outside Patio Lights G + entities: + - light.outside_patio_lights + - light.outside_patio_lights_2 diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index d9067b4b35f..c1062a488ee 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -3,10 +3,12 @@ import logging from os import path from homeassistant import config +from homeassistant.const import SERVICE_RELOAD from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.reload import ( async_get_platform, async_reload_integration_platforms, + async_setup_reload_service, ) from tests.async_mock import Mock, patch @@ -59,5 +61,43 @@ async def test_reload_platform(hass): assert len(setup_called) == 2 +async def test_setup_reload_service(hass): + """Test setting up a reload service.""" + component_setup = Mock(return_value=True) + + setup_called = [] + + async def setup_platform(*args): + setup_called.append(args) + + mock_integration(hass, MockModule(DOMAIN, setup=component_setup)) + mock_integration(hass, MockModule(PLATFORM, dependencies=[DOMAIN])) + + mock_platform = MockPlatform(async_setup_platform=setup_platform) + mock_entity_platform(hass, f"{DOMAIN}.{PLATFORM}", mock_platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_setup({DOMAIN: {"platform": PLATFORM, "sensors": None}}) + await hass.async_block_till_done() + assert component_setup.called + + assert f"{DOMAIN}.{PLATFORM}" in hass.config.components + assert len(setup_called) == 1 + + await async_setup_reload_service(hass, PLATFORM, [DOMAIN]) + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "helpers/reload_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + PLATFORM, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(setup_called) == 2 + + def _get_fixtures_base_path(): return path.dirname(path.dirname(__file__)) From 758c0adb5e3342755ebf2cfb83cacbb62769d60c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 26 Aug 2020 01:55:55 +0200 Subject: [PATCH 346/862] Rename entity base class for HMIPC (#39243) --- .../components/homematicip_cloud/__init__.py | 2 +- .../homematicip_cloud/alarm_control_panel.py | 10 ++-- .../homematicip_cloud/binary_sensor.py | 56 +++++++++---------- .../components/homematicip_cloud/climate.py | 6 +- .../components/homematicip_cloud/cover.py | 14 ++--- .../{device.py => generic_entity.py} | 18 +++--- .../components/homematicip_cloud/hap.py | 6 +- .../components/homematicip_cloud/light.py | 42 +++++++------- .../components/homematicip_cloud/sensor.py | 56 +++++++++---------- .../components/homematicip_cloud/switch.py | 26 ++++----- .../components/homematicip_cloud/weather.py | 16 +++--- tests/components/homematicip_cloud/helper.py | 2 +- .../homematicip_cloud/test_binary_sensor.py | 2 +- .../homematicip_cloud/test_sensor.py | 2 +- .../homematicip_cloud/test_switch.py | 2 +- 15 files changed, 130 insertions(+), 130 deletions(-) rename homeassistant/components/homematicip_cloud/{device.py => generic_entity.py} (93%) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index df1f5062fce..47da33e86da 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -18,7 +18,7 @@ from .const import ( HMIPC_HAPID, HMIPC_NAME, ) -from .device import HomematicipGenericDevice # noqa: F401 +from .generic_entity import HomematicipGenericEntity # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 from .services import async_setup_services, async_unload_services diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index b53e0363a6a..51cec6ac0cd 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -36,7 +36,7 @@ async def async_setup_entry( class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): - """Representation of an alarm control panel.""" + """Representation of the HomematicIP alarm control panel.""" def __init__(self, hap: HomematicipHAP) -> None: """Initialize the alarm control panel.""" @@ -56,7 +56,7 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): @property def state(self) -> str: - """Return the state of the device.""" + """Return the state of the alarm control panel.""" # check for triggered alarm if self._security_and_alarm.alarmActive: return STATE_ALARM_TRIGGERED @@ -98,7 +98,7 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): @callback def _async_device_changed(self, *args, **kwargs) -> None: - """Handle device state changes.""" + """Handle entity state changes.""" # Don't update disabled entities if self.enabled: _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) @@ -111,7 +111,7 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): @property def name(self) -> str: - """Return the name of the generic device.""" + """Return the name of the generic entity.""" name = CONST_ALARM_CONTROL_PANEL_NAME if self._home.name: name = f"{self._home.name} {name}" @@ -124,7 +124,7 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): @property def available(self) -> bool: - """Device available.""" + """Return if alarm control panel is available.""" return self._home.connected @property diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 15c41be24b5..aae7f881be0 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -41,7 +41,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -131,8 +131,8 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud acceleration sensor.""" +class HomematicipAccelerationSensor(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP acceleration sensor.""" @property def device_class(self) -> str: @@ -157,8 +157,8 @@ class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorEntity return state_attr -class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud contact interface.""" +class HomematicipContactInterface(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP contact interface.""" @property def device_class(self) -> str: @@ -173,8 +173,8 @@ class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorEntity): return self._device.windowState != WindowState.CLOSED -class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud shutter contact.""" +class HomematicipShutterContact(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP shutter contact.""" @property def device_class(self) -> str: @@ -189,8 +189,8 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorEntity): return self._device.windowState != WindowState.CLOSED -class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud motion detector.""" +class HomematicipMotionDetector(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP motion detector.""" @property def device_class(self) -> str: @@ -203,8 +203,8 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorEntity): return self._device.motionDetected -class HomematicipPresenceDetector(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud presence detector.""" +class HomematicipPresenceDetector(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP presence detector.""" @property def device_class(self) -> str: @@ -217,8 +217,8 @@ class HomematicipPresenceDetector(HomematicipGenericDevice, BinarySensorEntity): return self._device.presenceDetected -class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud smoke detector.""" +class HomematicipSmokeDetector(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP smoke detector.""" @property def device_class(self) -> str: @@ -236,8 +236,8 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorEntity): return False -class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud water detector.""" +class HomematicipWaterDetector(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP water detector.""" @property def device_class(self) -> str: @@ -250,8 +250,8 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorEntity): return self._device.moistureDetected or self._device.waterlevelDetected -class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud storm sensor.""" +class HomematicipStormSensor(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP storm sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize storm sensor.""" @@ -268,8 +268,8 @@ class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorEntity): return self._device.storm -class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud rain sensor.""" +class HomematicipRainSensor(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP rain sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize rain sensor.""" @@ -286,8 +286,8 @@ class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorEntity): return self._device.raining -class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud sunshine sensor.""" +class HomematicipSunshineSensor(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP sunshine sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize sunshine sensor.""" @@ -315,8 +315,8 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorEntity): return state_attr -class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud low battery sensor.""" +class HomematicipBatterySensor(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP low battery sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize battery sensor.""" @@ -334,9 +334,9 @@ class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorEntity): class HomematicipPluggableMainsFailureSurveillanceSensor( - HomematicipGenericDevice, BinarySensorEntity + HomematicipGenericEntity, BinarySensorEntity ): - """Representation of a HomematicIP Cloud pluggable mains failure surveillance sensor.""" + """Representation of the HomematicIP pluggable mains failure surveillance sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize pluggable mains failure surveillance sensor.""" @@ -353,8 +353,8 @@ class HomematicipPluggableMainsFailureSurveillanceSensor( return not self._device.powerMainsFailure -class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorEntity): - """Representation of a HomematicIP Cloud security zone group.""" +class HomematicipSecurityZoneSensorGroup(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP security zone sensor group.""" def __init__(self, hap: HomematicipHAP, device, post: str = "SecurityZone") -> None: """Initialize security zone group.""" @@ -411,7 +411,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorE class HomematicipSecuritySensorGroup( HomematicipSecurityZoneSensorGroup, BinarySensorEntity ): - """Representation of a HomematicIP security group.""" + """Representation of the HomematicIP security group.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize security group.""" diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 3d01f5d69fd..3f9cdcbcbdd 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -27,7 +27,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2} @@ -57,8 +57,8 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateEntity): - """Representation of a HomematicIP heating group. +class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): + """Representation of the HomematicIP heating group. Heat mode is supported for all heating devices incl. their defined profiles. Boost is available for radiator thermostats only. diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index d11a08d80a6..5b2f02e8a2d 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -19,7 +19,7 @@ from homeassistant.components.cover import ( from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -54,8 +54,8 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipCoverShutter(HomematicipGenericDevice, CoverEntity): - """Representation of a HomematicIP Cloud cover shutter device.""" +class HomematicipCoverShutter(HomematicipGenericEntity, CoverEntity): + """Representation of the HomematicIP cover shutter.""" @property def current_cover_position(self) -> int: @@ -92,7 +92,7 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverEntity): class HomematicipCoverSlats(HomematicipCoverShutter, CoverEntity): - """Representation of a HomematicIP Cloud cover slats device.""" + """Representation of the HomematicIP cover slats.""" @property def current_cover_tilt_position(self) -> int: @@ -121,8 +121,8 @@ class HomematicipCoverSlats(HomematicipCoverShutter, CoverEntity): await self._device.set_shutter_stop() -class HomematicipGarageDoorModule(HomematicipGenericDevice, CoverEntity): - """Representation of a HomematicIP Garage Door Module.""" +class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): + """Representation of the HomematicIP Garage Door Module.""" @property def current_cover_position(self) -> int: @@ -154,7 +154,7 @@ class HomematicipGarageDoorModule(HomematicipGenericDevice, CoverEntity): class HomematicipCoverShutterGroup(HomematicipCoverSlats, CoverEntity): - """Representation of a HomematicIP Cloud cover shutter group.""" + """Representation of the HomematicIP cover shutter group.""" def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None: """Initialize switching group.""" diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/generic_entity.py similarity index 93% rename from homeassistant/components/homematicip_cloud/device.py rename to homeassistant/components/homematicip_cloud/generic_entity.py index 91bec464a29..7450a82943f 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -1,4 +1,4 @@ -"""Generic device for the HomematicIP Cloud component.""" +"""Generic entity for the HomematicIP Cloud component.""" import logging from typing import Any, Dict, Optional @@ -65,11 +65,11 @@ GROUP_ATTRIBUTES = { } -class HomematicipGenericDevice(Entity): - """Representation of an HomematicIP generic device.""" +class HomematicipGenericEntity(Entity): + """Representation of the HomematicIP generic entity.""" def __init__(self, hap: HomematicipHAP, device, post: Optional[str] = None) -> None: - """Initialize the generic device.""" + """Initialize the generic entity.""" self._hap = hap self._home = hap.home self._device = device @@ -117,7 +117,7 @@ class HomematicipGenericDevice(Entity): ) async def async_will_remove_from_hass(self) -> None: - """Run when entity will be removed from hass.""" + """Run when hmip device will be removed from hass.""" # Only go further if the device/entity should be removed from registries # due to a removal of the HmIP device. @@ -127,7 +127,7 @@ class HomematicipGenericDevice(Entity): del self._hap.hmip_device_by_entity_id[self.entity_id] await self.async_remove_from_registries() except KeyError as err: - _LOGGER.debug("Error removing HMIP entity from registry: %s", err) + _LOGGER.debug("Error removing HMIP device from registry: %s", err) async def async_remove_from_registries(self) -> None: """Remove entity/device from registry.""" @@ -164,7 +164,7 @@ class HomematicipGenericDevice(Entity): @property def name(self) -> str: - """Return the name of the generic device.""" + """Return the name of the generic entity.""" name = self._device.label if name and self._home.name: name = f"{self._home.name} {name}" @@ -186,7 +186,7 @@ class HomematicipGenericDevice(Entity): @property def available(self) -> bool: - """Device available.""" + """Return if entity is available.""" return not self._device.unreach @property @@ -205,7 +205,7 @@ class HomematicipGenericDevice(Entity): @property def device_state_attributes(self) -> Dict[str, Any]: - """Return the state attributes of the generic device.""" + """Return the state attributes of the generic entity.""" state_attr = {} if isinstance(self._device, AsyncDevice): diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index dd85827f1ae..431b05e692a 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -117,7 +117,7 @@ class HomematicipHAP: Triggered when the HMIP HOME_CHANGED event has fired. There are several occasions for this event to happen. 1. We are interested to check whether the access point - is still connected. If not, device state changes cannot + is still connected. If not, entity state changes cannot be forwarded to hass. So if access point is disconnected all devices are set to unavailable. 2. We need to update home including devices and groups after a reconnect. @@ -131,7 +131,7 @@ class HomematicipHAP: elif not self._accesspoint_connected: # Now the HOME_CHANGED event has fired indicating the access # point has reconnected to the cloud again. - # Explicitly getting an update as device states might have + # Explicitly getting an update as entity states might have # changed during access point disconnect.""" job = self.hass.async_create_task(self.get_state()) @@ -140,7 +140,7 @@ class HomematicipHAP: @callback def async_create_entity(self, *args, **kwargs) -> None: - """Create a device or a group.""" + """Create an entity or a group.""" is_device = EventType(kwargs["event_type"]) == EventType.DEVICE_ADDED self.hass.async_create_task(self.async_create_entity_lazy(is_device)) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 9ddcc44e8bd..72737122372 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -26,7 +26,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -64,11 +64,11 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipLight(HomematicipGenericDevice, LightEntity): - """Representation of a HomematicIP Cloud light device.""" +class HomematicipLight(HomematicipGenericEntity, LightEntity): + """Representation of the HomematicIP light.""" def __init__(self, hap: HomematicipHAP, device) -> None: - """Initialize the light device.""" + """Initialize the light entity.""" super().__init__(hap, device) @property @@ -81,24 +81,24 @@ class HomematicipLight(HomematicipGenericDevice, LightEntity): @property def is_on(self) -> bool: - """Return true if device is on.""" + """Return true if light is on.""" return self._device.on async def async_turn_on(self, **kwargs) -> None: - """Turn the device on.""" + """Turn the light on.""" await self._device.turn_on() async def async_turn_off(self, **kwargs) -> None: - """Turn the device off.""" + """Turn the light off.""" await self._device.turn_off() class HomematicipLightMeasuring(HomematicipLight): - """Representation of a HomematicIP Cloud measuring light device.""" + """Representation of the HomematicIP measuring light.""" @property def device_state_attributes(self) -> Dict[str, Any]: - """Return the state attributes of the generic device.""" + """Return the state attributes of the light.""" state_attr = super().device_state_attributes current_power_w = self._device.currentPowerConsumption @@ -110,16 +110,16 @@ class HomematicipLightMeasuring(HomematicipLight): return state_attr -class HomematicipDimmer(HomematicipGenericDevice, LightEntity): - """Representation of HomematicIP Cloud dimmer light device.""" +class HomematicipDimmer(HomematicipGenericEntity, LightEntity): + """Representation of HomematicIP Cloud dimmer.""" def __init__(self, hap: HomematicipHAP, device) -> None: - """Initialize the dimmer light device.""" + """Initialize the dimmer light entity.""" super().__init__(hap, device) @property def is_on(self) -> bool: - """Return true if device is on.""" + """Return true if dimmer is on.""" return self._device.dimLevel is not None and self._device.dimLevel > 0.0 @property @@ -133,22 +133,22 @@ class HomematicipDimmer(HomematicipGenericDevice, LightEntity): return SUPPORT_BRIGHTNESS async def async_turn_on(self, **kwargs) -> None: - """Turn the light on.""" + """Turn the dimmer on.""" if ATTR_BRIGHTNESS in kwargs: await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS] / 255.0) else: await self._device.set_dim_level(1) async def async_turn_off(self, **kwargs) -> None: - """Turn the light off.""" + """Turn the dimmer off.""" await self._device.set_dim_level(0) -class HomematicipNotificationLight(HomematicipGenericDevice, LightEntity): - """Representation of HomematicIP Cloud dimmer light device.""" +class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): + """Representation of HomematicIP Cloud notification light.""" def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: - """Initialize the dimmer light device.""" + """Initialize the notification light entity.""" self.channel = channel if self.channel == 2: super().__init__(hap, device, "Top") @@ -171,7 +171,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, LightEntity): @property def is_on(self) -> bool: - """Return true if device is on.""" + """Return true if light is on.""" return ( self._func_channel.dimLevel is not None and self._func_channel.dimLevel > 0.0 @@ -190,7 +190,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, LightEntity): @property def device_state_attributes(self) -> Dict[str, Any]: - """Return the state attributes of the generic device.""" + """Return the state attributes of the notification light sensor.""" state_attr = super().device_state_attributes if self.is_on: @@ -200,7 +200,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, LightEntity): @property def name(self) -> str: - """Return the name of the generic device.""" + """Return the name of the notification light sensor.""" label = self._get_label_by_channel(self.channel) if label: return label diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index a45591ecc30..c31e4d73cca 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -37,8 +37,8 @@ from homeassistant.const import ( ) from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice -from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity +from .generic_entity import ATTR_IS_GROUP, ATTR_MODEL_TYPE from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -120,11 +120,11 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipAccesspointStatus(HomematicipGenericDevice): - """Representation of an HomeMaticIP Cloud access point.""" +class HomematicipAccesspointStatus(HomematicipGenericEntity): + """Representation of then HomeMaticIP access point.""" def __init__(self, hap: HomematicipHAP) -> None: - """Initialize access point device.""" + """Initialize access point status entity.""" super().__init__(hap, hap.home) @property @@ -140,7 +140,7 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): @property def icon(self) -> str: - """Return the icon of the access point device.""" + """Return the icon of the access point entity.""" return "mdi:access-point-network" @property @@ -150,7 +150,7 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): @property def available(self) -> bool: - """Device available.""" + """Return if access point is available.""" return self._home.connected @property @@ -169,8 +169,8 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): return state_attr -class HomematicipHeatingThermostat(HomematicipGenericDevice): - """Representation of a HomematicIP heating thermostat device.""" +class HomematicipHeatingThermostat(HomematicipGenericEntity): + """Representation of the HomematicIP heating thermostat.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize heating thermostat device.""" @@ -198,8 +198,8 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): return UNIT_PERCENTAGE -class HomematicipHumiditySensor(HomematicipGenericDevice): - """Representation of a HomematicIP Cloud humidity device.""" +class HomematicipHumiditySensor(HomematicipGenericEntity): + """Representation of the HomematicIP humidity sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" @@ -221,8 +221,8 @@ class HomematicipHumiditySensor(HomematicipGenericDevice): return UNIT_PERCENTAGE -class HomematicipTemperatureSensor(HomematicipGenericDevice): - """Representation of a HomematicIP Cloud thermometer device.""" +class HomematicipTemperatureSensor(HomematicipGenericEntity): + """Representation of the HomematicIP thermometer.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" @@ -258,8 +258,8 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): return state_attr -class HomematicipIlluminanceSensor(HomematicipGenericDevice): - """Representation of a HomematicIP Illuminance device.""" +class HomematicipIlluminanceSensor(HomematicipGenericEntity): + """Representation of the HomematicIP Illuminance sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" @@ -296,8 +296,8 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): return state_attr -class HomematicipPowerSensor(HomematicipGenericDevice): - """Representation of a HomematicIP power measuring device.""" +class HomematicipPowerSensor(HomematicipGenericEntity): + """Representation of the HomematicIP power measuring sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" @@ -310,7 +310,7 @@ class HomematicipPowerSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Representation of the HomematicIP power consumption value.""" + """Return the power consumption value.""" return self._device.currentPowerConsumption @property @@ -319,16 +319,16 @@ class HomematicipPowerSensor(HomematicipGenericDevice): return POWER_WATT -class HomematicipWindspeedSensor(HomematicipGenericDevice): - """Representation of a HomematicIP wind speed sensor.""" +class HomematicipWindspeedSensor(HomematicipGenericEntity): + """Representation of the HomematicIP wind speed sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: - """Initialize the device.""" + """Initialize the windspeed sensor.""" super().__init__(hap, device, "Windspeed") @property def state(self) -> float: - """Representation of the HomematicIP wind speed value.""" + """Return the wind speed value.""" return self._device.windSpeed @property @@ -352,8 +352,8 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): return state_attr -class HomematicipTodayRainSensor(HomematicipGenericDevice): - """Representation of a HomematicIP rain counter of a day sensor.""" +class HomematicipTodayRainSensor(HomematicipGenericEntity): + """Representation of the HomematicIP rain counter of a day sensor.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" @@ -361,7 +361,7 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Representation of the HomematicIP today's rain value.""" + """Return the today's rain value.""" return round(self._device.todayRainCounter, 2) @property @@ -370,12 +370,12 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): return "mm" -class HomematicipPassageDetectorDeltaCounter(HomematicipGenericDevice): - """Representation of a HomematicIP passage detector delta counter.""" +class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity): + """Representation of the HomematicIP passage detector delta counter.""" @property def state(self) -> int: - """Representation of the HomematicIP passage detector delta counter value.""" + """Return the passage detector delta counter value.""" return self._device.leftRightCounterDelta @property diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index a63602a6922..64ee862b2d2 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -20,8 +20,8 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity +from .generic_entity import ATTR_GROUP_MEMBER_UNREACHABLE from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ async def async_setup_entry( for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring - # This device is implemented in the light platform and will + # This entity is implemented in the light platform and will # not be added in the switch platform pass elif isinstance( @@ -73,8 +73,8 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipSwitch(HomematicipGenericDevice, SwitchEntity): - """representation of a HomematicIP Cloud switch device.""" +class HomematicipSwitch(HomematicipGenericEntity, SwitchEntity): + """Representation of the HomematicIP switch.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the switch device.""" @@ -94,8 +94,8 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchEntity): await self._device.turn_off() -class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchEntity): - """representation of a HomematicIP switching group.""" +class HomematicipGroupSwitch(HomematicipGenericEntity, SwitchEntity): + """Representation of the HomematicIP switching group.""" def __init__(self, hap: HomematicipHAP, device, post: str = "Group") -> None: """Initialize switching group.""" @@ -136,7 +136,7 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchEntity): class HomematicipSwitchMeasuring(HomematicipSwitch): - """Representation of a HomematicIP measuring switch device.""" + """Representation of the HomematicIP measuring switch.""" @property def current_power_w(self) -> float: @@ -151,8 +151,8 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): return round(self._device.energyCounter) -class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchEntity): - """Representation of a HomematicIP Cloud multi switch device.""" +class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity): + """Representation of the HomematicIP multi switch.""" def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: """Initialize the multi switch device.""" @@ -174,13 +174,13 @@ class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchEntity): @property def is_on(self) -> bool: - """Return true if device is on.""" + """Return true if switch is on.""" return self._device.functionalChannels[self.channel].on async def async_turn_on(self, **kwargs) -> None: - """Turn the device on.""" + """Turn the switch on.""" await self._device.turn_on(self.channel) async def async_turn_off(self, **kwargs) -> None: - """Turn the device off.""" + """Turn the switch off.""" await self._device.turn_off(self.channel) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 04f3b06cbb0..c845343c030 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -55,8 +55,8 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): - """representation of a HomematicIP Cloud weather sensor plus & basic.""" +class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): + """Representation of the HomematicIP weather sensor plus & basic.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the weather sensor.""" @@ -105,7 +105,7 @@ class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): class HomematicipWeatherSensorPro(HomematicipWeatherSensor): - """representation of a HomematicIP weather sensor pro.""" + """Representation of the HomematicIP weather sensor pro.""" @property def wind_bearing(self) -> float: @@ -113,8 +113,8 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): return self._device.windDirection -class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): - """representation of a HomematicIP Cloud home weather.""" +class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity): + """Representation of the HomematicIP home weather.""" def __init__(self, hap: HomematicipHAP) -> None: """Initialize the home weather.""" @@ -123,7 +123,7 @@ class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): @property def available(self) -> bool: - """Device available.""" + """Return if weather entity is available.""" return self._home.connected @property @@ -133,7 +133,7 @@ class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): @property def temperature(self) -> float: - """Return the platform temperature.""" + """Return the temperature.""" return self._device.weather.temperature @property diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index fede095e57d..dbbf0249c0c 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -13,7 +13,7 @@ from homematicip.home import Home from homeassistant import config_entries from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.components.homematicip_cloud.device import ( +from homeassistant.components.homematicip_cloud.generic_entity import ( ATTR_IS_GROUP, ATTR_MODEL_TYPE, ) diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 7fe9a7327ea..cfde3413916 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -15,7 +15,7 @@ from homeassistant.components.homematicip_cloud.binary_sensor import ( ATTR_WATER_LEVEL_DETECTED, ATTR_WINDOW_STATE, ) -from homeassistant.components.homematicip_cloud.device import ( +from homeassistant.components.homematicip_cloud.generic_entity import ( ATTR_EVENT_DELAY, ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_LOW_BATTERY, diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 61de66d916d..2cf7c8dff7b 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -2,7 +2,7 @@ from homematicip.base.enums import ValveState from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.components.homematicip_cloud.device import ( +from homeassistant.components.homematicip_cloud.generic_entity import ( ATTR_CONFIG_PENDING, ATTR_DEVICE_OVERHEATED, ATTR_DEVICE_OVERLOADED, diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index 59690458881..85bfaaa4ec5 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -1,6 +1,6 @@ """Tests for HomematicIP Cloud switch.""" from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.components.homematicip_cloud.device import ( +from homeassistant.components.homematicip_cloud.generic_entity import ( ATTR_GROUP_MEMBER_UNREACHABLE, ) from homeassistant.components.switch import ( From 5018e53b33382e10c8465d87cd8fa483b864dbd9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 19:05:29 -0500 Subject: [PATCH 347/862] Add the ability to reload the rest platforms from yaml (#39257) * Add the ability to reload rest platforms from yaml * Revert changes to notify as these will be done in another pass --- homeassistant/components/rest/__init__.py | 3 ++ .../components/rest/binary_sensor.py | 5 ++ homeassistant/components/rest/sensor.py | 5 ++ homeassistant/components/rest/services.yaml | 2 + homeassistant/components/rest/switch.py | 6 +++ tests/components/rest/test_sensor.py | 53 ++++++++++++++++++- tests/fixtures/rest/configuration.yaml | 5 ++ 7 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/rest/services.yaml create mode 100644 tests/fixtures/rest/configuration.yaml diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index fcdf39e8398..e0b743c1d37 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -1 +1,4 @@ """The rest component.""" + +DOMAIN = "rest" +PLATFORMS = ["binary_sensor", "sensor", "switch"] diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index a78c6aa5f2b..c5b8f16162a 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -29,7 +29,9 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service +from . import DOMAIN, PLATFORMS from .sensor import RestData _LOGGER = logging.getLogger(__name__) @@ -68,6 +70,9 @@ PLATFORM_SCHEMA = vol.All( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the REST binary sensor.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + name = config.get(CONF_NAME) resource = config.get(CONF_RESOURCE) resource_template = config.get(CONF_RESOURCE_TEMPLATE) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 2c8df9625cd..9925eb016cb 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -33,6 +33,9 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.reload import setup_reload_service + +from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -78,6 +81,8 @@ PLATFORM_SCHEMA = vol.All( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RESTful sensor.""" + setup_reload_service(hass, DOMAIN, PLATFORMS) + name = config.get(CONF_NAME) resource = config.get(CONF_RESOURCE) resource_template = config.get(CONF_RESOURCE_TEMPLATE) diff --git a/homeassistant/components/rest/services.yaml b/homeassistant/components/rest/services.yaml new file mode 100644 index 00000000000..717859d0c12 --- /dev/null +++ b/homeassistant/components/rest/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all rest entities. diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index ab880201072..6a9b4b2ac5b 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -21,6 +21,9 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service + +from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -58,6 +61,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the RESTful switch.""" + + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + body_off = config.get(CONF_BODY_OFF) body_on = config.get(CONF_BODY_ON) is_on_template = config.get(CONF_IS_ON_TEMPLATE) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 90a8b8d361e..39197680887 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the REST sensor platform.""" +from os import path import unittest import pytest @@ -8,12 +9,13 @@ from requests.exceptions import RequestException, Timeout from requests.structures import CaseInsensitiveDict import requests_mock +from homeassistant import config as hass_config import homeassistant.components.rest.sensor as rest import homeassistant.components.sensor as sensor -from homeassistant.const import DATA_MEGABYTES +from homeassistant.const import DATA_MEGABYTES, SERVICE_RELOAD from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.config_validation import template -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component, setup_component from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant @@ -677,3 +679,50 @@ class TestRestData(unittest.TestCase): """Test update when a request exception occurs.""" self.rest.update() assert self.rest.data is None + + +async def test_reload(hass): + """Verify we can reload reset sensors.""" + + with requests_mock.Mocker() as mock_req: + mock_req.get("http://localhost", text="test data") + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "rest", + "method": "GET", + "name": "mockrest", + "resource": "http://localhost", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("sensor.mockrest") + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "rest/configuration.yaml", + ) + with patch.object( + hass_config, "YAML_CONFIG_FILE", yaml_path + ), requests_mock.Mocker() as mock_req: + mock_req.get("http://localhost", text="test data 2") + await hass.services.async_call( + "rest", SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("sensor.mockreset") is None + assert hass.states.get("sensor.rollout") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/rest/configuration.yaml b/tests/fixtures/rest/configuration.yaml new file mode 100644 index 00000000000..7848a429026 --- /dev/null +++ b/tests/fixtures/rest/configuration.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: rest + resource: "http://localhost" + method: GET + name: rollout From 215e3f2dab7611f369efe6de441a7aba745854ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 19:52:36 -0500 Subject: [PATCH 348/862] Add the ability to reload command_line platforms from yaml (#39262) --- .../components/command_line/binary_sensor.py | 6 ++- .../components/command_line/const.py | 2 + .../components/command_line/cover.py | 6 ++- .../components/command_line/sensor.py | 6 ++- .../components/command_line/services.yaml | 2 + .../components/command_line/switch.py | 6 ++- tests/components/command_line/test_cover.py | 40 +++++++++++++++++++ .../fixtures/command_line/configuration.yaml | 6 +++ 8 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/command_line/services.yaml create mode 100644 tests/fixtures/command_line/configuration.yaml diff --git a/homeassistant/components/command_line/binary_sensor.py b/homeassistant/components/command_line/binary_sensor.py index 86916e86a26..cb78b7b9144 100644 --- a/homeassistant/components/command_line/binary_sensor.py +++ b/homeassistant/components/command_line/binary_sensor.py @@ -18,8 +18,9 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS from .sensor import CommandSensorData _LOGGER = logging.getLogger(__name__) @@ -46,6 +47,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Command line Binary Sensor.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + name = config.get(CONF_NAME) command = config.get(CONF_COMMAND) payload_off = config.get(CONF_PAYLOAD_OFF) diff --git a/homeassistant/components/command_line/const.py b/homeassistant/components/command_line/const.py index 8c5bc0b2967..2ac6aab29a5 100644 --- a/homeassistant/components/command_line/const.py +++ b/homeassistant/components/command_line/const.py @@ -2,3 +2,5 @@ CONF_COMMAND_TIMEOUT = "command_timeout" DEFAULT_TIMEOUT = 15 +DOMAIN = "command_line" +PLATFORMS = ["binary_sensor", "cover", "sensor", "switch"] diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 1fdcdf3b3e7..05d2b9634f2 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -14,9 +14,10 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service from . import call_shell_with_timeout, check_output_or_log -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -39,6 +40,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up cover controlled by shell commands.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + devices = config.get(CONF_COVERS, {}) covers = [] diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 778806099aa..35f7c5a4811 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -18,9 +18,10 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.reload import setup_reload_service from . import check_output_or_log -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -44,6 +45,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Command Sensor.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + name = config.get(CONF_NAME) command = config.get(CONF_COMMAND) unit = config.get(CONF_UNIT_OF_MEASUREMENT) diff --git a/homeassistant/components/command_line/services.yaml b/homeassistant/components/command_line/services.yaml new file mode 100644 index 00000000000..8876e8dc925 --- /dev/null +++ b/homeassistant/components/command_line/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all command_line entities. diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 804e3c6a4d5..ce46cd4f2cd 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -17,9 +17,10 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service from . import call_shell_with_timeout, check_output_or_log -from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT +from .const import CONF_COMMAND_TIMEOUT, DEFAULT_TIMEOUT, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -41,6 +42,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return switches controlled by shell commands.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + devices = config.get(CONF_SWITCHES, {}) switches = [] diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index cc91e521d68..4707296a426 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -1,16 +1,20 @@ """The tests the cover command line platform.""" import os +from os import path import tempfile from unittest import mock +from asynctest.mock import patch import pytest +from homeassistant import config as hass_config import homeassistant.components.command_line.cover as cmd_rs from homeassistant.components.cover import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, + SERVICE_RELOAD, SERVICE_STOP_COVER, ) from homeassistant.setup import async_setup_component @@ -87,3 +91,39 @@ async def test_state_value(hass): DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True ) assert "closed" == hass.states.get("cover.test").state + + +async def test_reload(hass): + """Verify we can reload command_line covers.""" + + test_cover = { + "command_state": "echo open", + "value_template": "{{ value }}", + } + await async_setup_component( + hass, + DOMAIN, + {"cover": {"platform": "command_line", "covers": {"test": test_cover}}}, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + assert hass.states.get("cover.test").state + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "command_line/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "command_line", SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("cover.test") is None + assert hass.states.get("cover.from_yaml") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/command_line/configuration.yaml b/tests/fixtures/command_line/configuration.yaml new file mode 100644 index 00000000000..f210b640338 --- /dev/null +++ b/tests/fixtures/command_line/configuration.yaml @@ -0,0 +1,6 @@ +cover: + - platform: command_line + covers: + from_yaml: + command_state: "echo closed" + value_template: "{{ value }}" From eaac00acfcaa1dac1286eb184416a6bed17a3238 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2020 20:55:45 -0500 Subject: [PATCH 349/862] Add the ability to reload filter platforms from yaml (#39267) * Add the ability to reload filter platforms from yaml * force in memory db * fix listener leak on un-load --- homeassistant/components/filter/__init__.py | 3 + homeassistant/components/filter/sensor.py | 12 +++- homeassistant/components/filter/services.yaml | 2 + tests/components/filter/test_sensor.py | 56 ++++++++++++++++++- tests/fixtures/filter/configuration.yaml | 11 ++++ 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/filter/services.yaml create mode 100644 tests/fixtures/filter/configuration.yaml diff --git a/homeassistant/components/filter/__init__.py b/homeassistant/components/filter/__init__.py index ebdcec75abf..35e374771c2 100644 --- a/homeassistant/components/filter/__init__.py +++ b/homeassistant/components/filter/__init__.py @@ -1 +1,4 @@ """The filter component.""" + +DOMAIN = "filter" +PLATFORMS = ["sensor"] diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index e46af9fa138..b0cc08bc945 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -25,9 +25,12 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util +from . import DOMAIN, PLATFORMS + _LOGGER = logging.getLogger(__name__) FILTER_NAME_RANGE = "range" @@ -150,6 +153,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the template sensors.""" + + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + name = config.get(CONF_NAME) entity_id = config.get(CONF_ENTITY_ID) @@ -279,8 +285,10 @@ class SensorFilter(Entity): for state in history_list: self._update_filter_sensor_state(state, False) - async_track_state_change_event( - self.hass, [self._entity], self._update_filter_sensor_state_event + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._entity], self._update_filter_sensor_state_event + ) ) @property diff --git a/homeassistant/components/filter/services.yaml b/homeassistant/components/filter/services.yaml new file mode 100644 index 00000000000..6f6ea1b04d6 --- /dev/null +++ b/homeassistant/components/filter/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all filter entities. diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index f6eae30c653..84db16c1464 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -1,8 +1,11 @@ """The test for the data filter sensor platform.""" from datetime import timedelta +from os import path import unittest +from homeassistant import config as hass_config from homeassistant.components.filter.sensor import ( + DOMAIN, LowPassFilter, OutlierFilter, RangeFilter, @@ -10,8 +13,9 @@ from homeassistant.components.filter.sensor import ( TimeSMAFilter, TimeThrottleFilter, ) +from homeassistant.const import SERVICE_RELOAD import homeassistant.core as ha -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util from tests.async_mock import patch @@ -307,3 +311,53 @@ class TestFilterSensor(unittest.TestCase): for state in self.values: filtered = filt.filter_state(state) assert 21.5 == filtered.state + + +async def test_reload(hass): + """Verify we can reload filter sensors.""" + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db + + hass.states.async_set("sensor.test_monitored", 12345) + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + {"filter": "lowpass", "time_constant": 10, "precision": 2}, + {"filter": "throttle", "window_size": 1}, + ], + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("sensor.test") + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "filter/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("sensor.test") is None + assert hass.states.get("sensor.filtered_realistic_humidity") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/filter/configuration.yaml b/tests/fixtures/filter/configuration.yaml new file mode 100644 index 00000000000..c29c504b2ee --- /dev/null +++ b/tests/fixtures/filter/configuration.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: filter + name: "filtered realistic humidity" + entity_id: sensor.realistic_humidity + filters: + - filter: outlier + window_size: 4 + radius: 4.0 + - filter: lowpass + time_constant: 10 + precision: 2 From 2a9da208d49017883c2f6f13ca309d27d0d01f09 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Aug 2020 10:20:14 +0200 Subject: [PATCH 350/862] Allow disabling integrations in manifest, block uuid package being installed and disable ezviz (#38444) --- .github/workflows/ci.yaml | 4 ++-- homeassistant/components/ezviz/camera.py | 1 + homeassistant/components/ezviz/manifest.json | 1 + homeassistant/loader.py | 5 +++++ homeassistant/package_constraints.txt | 3 +++ homeassistant/setup.py | 4 ++++ requirements_all.txt | 3 --- script/gen_requirements_all.py | 6 ++++++ script/hassfest/manifest.py | 1 + script/hassfest/model.py | 5 +++++ tests/test_setup.py | 12 ++++++++++++ 11 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0f060724994..e337c019f52 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -46,7 +46,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip==20.1.1 setuptools + pip install -U pip setuptools pip install -r requirements.txt -r requirements_test.txt # Uninstalling typing as a workaround. Eventually we should make sure # all our dependencies drop typing. @@ -603,7 +603,7 @@ jobs: run: | python -m venv venv . venv/bin/activate - pip install -U pip==20.1.1 setuptools wheel + pip install -U pip setuptools wheel pip install -r requirements_all.txt pip install -r requirements_test.txt # Uninstalling typing as a workaround. Eventually we should make sure diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index e7e6725e455..701af451496 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -2,6 +2,7 @@ import asyncio import logging +# pylint: disable=import-error from haffmpeg.tools import IMAGE_JPEG, ImageFrame from pyezviz.camera import EzvizCamera from pyezviz.client import EzvizClient, PyEzvizError diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 651fd77619c..03bdfc5217c 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -1,4 +1,5 @@ { + "disabled": "Dependency contains code that breaks Home Assistant.", "domain": "ezviz", "name": "Ezviz", "documentation": "https://www.home-assistant.io/integrations/ezviz", diff --git a/homeassistant/loader.py b/homeassistant/loader.py index b82f2c0109a..c5027710c47 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -271,6 +271,11 @@ class Integration: """Return name.""" return cast(str, self.manifest["name"]) + @property + def disabled(self) -> Optional[str]: + """Return reason integration is disabled.""" + return cast(Optional[str], self.manifest.get("disabled")) + @property def domain(self) -> str: """Return domain.""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c6a543e94d7..261dd5dd34d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -44,3 +44,6 @@ enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 + +# This is built-in and breaks pip if installed +uuid==1000000000.0.0 diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 578cd33b097..341229a83b1 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -124,6 +124,10 @@ async def _async_setup_component( log_error("Integration not found.") return False + if integration.disabled: + log_error(f"dependency is disabled - {integration.disabled}") + return False + # Validate all dependencies exist and there are no circular dependencies if not await integration.resolve_dependencies(): return False diff --git a/requirements_all.txt b/requirements_all.txt index e543e325592..de5c87a07c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1334,9 +1334,6 @@ pyephember==0.3.1 # homeassistant.components.everlights pyeverlights==0.1.0 -# homeassistant.components.ezviz -pyezviz==0.1.5 - # homeassistant.components.fido pyfido==2.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b851983b6f6..25d4d3d3e7e 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -71,6 +71,9 @@ enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 + +# This is built-in and breaks pip if installed +uuid==1000000000.0.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( @@ -178,6 +181,9 @@ def gather_requirements_from_manifests(errors, reqs): errors.append(f"The manifest for integration {domain} is invalid.") continue + if integration.disabled: + continue + process_requirements( errors, integration.requirements, f"homeassistant.components.{domain}", reqs ) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index cb592b63b53..cd3895f5f20 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -54,6 +54,7 @@ MANIFEST_SCHEMA = vol.Schema( vol.Optional("dependencies"): [str], vol.Optional("after_dependencies"): [str], vol.Required("codeowners"): [str], + vol.Optional("disabled"): str, } ) diff --git a/script/hassfest/model.py b/script/hassfest/model.py index bb438f4d84f..c993689aaab 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -73,6 +73,11 @@ class Integration: """Integration domain.""" return self.path.name + @property + def disabled(self) -> Optional[str]: + """List of disabled.""" + return self.manifest.get("disabled") + @property def requirements(self) -> List[str]: """List of requirements.""" diff --git a/tests/test_setup.py b/tests/test_setup.py index abd9cecd9ac..8651308572a 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -583,3 +583,15 @@ async def test_parallel_entry_setup(hass): await setup.async_setup_component(hass, "comp", {}) assert calls == [1, 2, 1, 2] + + +async def test_integration_disabled(hass, caplog): + """Test we can disable an integration.""" + disabled_reason = "Dependency contains code that breaks Home Assistant" + mock_integration( + hass, + MockModule("test_component1", partial_manifest={"disabled": disabled_reason}), + ) + result = await setup.async_setup_component(hass, "test_component1", {}) + assert not result + assert disabled_reason in caplog.text From e065673d7bf07364dd0ef2bbf6be7c1f07bd19de Mon Sep 17 00:00:00 2001 From: Marty Zalega Date: Wed, 26 Aug 2020 18:32:23 +1000 Subject: [PATCH 351/862] Version bump panasonic_viera to 0.3.6 (#39269) This version fixes the issue of mishandling an error when requesting a session id --- homeassistant/components/panasonic_viera/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index d046d742e93..dd61399bcd9 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -2,7 +2,7 @@ "domain": "panasonic_viera", "name": "Panasonic Viera", "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", - "requirements": ["panasonic_viera==0.3.5"], + "requirements": ["panasonic_viera==0.3.6"], "codeowners": ["@joogps"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index de5c87a07c7..714e0eaf028 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1045,7 +1045,7 @@ paho-mqtt==1.5.0 panacotta==0.1 # homeassistant.components.panasonic_viera -panasonic_viera==0.3.5 +panasonic_viera==0.3.6 # homeassistant.components.pcal9535a pcal9535a==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b5d1765455..f5e01ba8c77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -489,7 +489,7 @@ ovoenergy==1.1.7 paho-mqtt==1.5.0 # homeassistant.components.panasonic_viera -panasonic_viera==0.3.5 +panasonic_viera==0.3.6 # homeassistant.components.dunehd pdunehd==1.3.2 From 067efc780524e6cd2bdfa1f145d8704ad0ffe67b Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Wed, 26 Aug 2020 05:47:44 -0300 Subject: [PATCH 352/862] Remove services.yaml from the Broadlink integration (#39261) --- homeassistant/components/broadlink/services.yaml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 homeassistant/components/broadlink/services.yaml diff --git a/homeassistant/components/broadlink/services.yaml b/homeassistant/components/broadlink/services.yaml deleted file mode 100644 index f1b39976afc..00000000000 --- a/homeassistant/components/broadlink/services.yaml +++ /dev/null @@ -1,14 +0,0 @@ -send: - description: Send a raw packet to device. - fields: - host: - description: IP address of device to send packet via. This must be an already configured device. - example: "192.168.0.1" - packet: - description: base64 encoded packet. -learn: - description: Learn a IR or RF code from remote. - fields: - host: - description: IP address of device to send packet via. This must be an already configured device. - example: "192.168.0.1" From 2568932c1cd02c8adf63e2e01049e9bc6e7d8a84 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 26 Aug 2020 10:55:57 +0200 Subject: [PATCH 353/862] Bump brother library to version 0.1.15 (#39226) --- homeassistant/components/brother/const.py | 8 ++----- .../components/brother/manifest.json | 2 +- homeassistant/components/brother/sensor.py | 14 +++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/brother/test_sensor.py | 21 ++++++++++++------- 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 55c9989d60c..98229f5c7a2 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -1,5 +1,5 @@ """Constants for Brother integration.""" -from homeassistant.const import TIME_DAYS, UNIT_PERCENTAGE +from homeassistant.const import UNIT_PERCENTAGE ATTR_BELT_UNIT_REMAINING_LIFE = "belt_unit_remaining_life" ATTR_BLACK_DRUM_COUNTER = "black_drum_counter" @@ -162,9 +162,5 @@ SENSOR_TYPES = { ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), ATTR_UNIT: UNIT_PERCENTAGE, }, - ATTR_UPTIME: { - ATTR_ICON: "mdi:timer-outline", - ATTR_LABEL: ATTR_UPTIME.title(), - ATTR_UNIT: TIME_DAYS, - }, + ATTR_UPTIME: {ATTR_ICON: None, ATTR_LABEL: ATTR_UPTIME.title(), ATTR_UNIT: None}, } diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 7f59aaa9c2c..2d3163b125a 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==0.1.14"], + "requirements": ["brother==0.1.15"], "zeroconf": ["_printer._tcp.local."], "config_flow": true, "quality_scale": "platinum" diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index d4f389908b1..607a5989abc 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -1,7 +1,10 @@ """Support for the Brother service.""" +from datetime import timedelta import logging +from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.entity import Entity +from homeassistant.util.dt import utcnow from .const import ( ATTR_BLACK_DRUM_COUNTER, @@ -20,6 +23,7 @@ from .const import ( ATTR_MAGENTA_DRUM_REMAINING_PAGES, ATTR_MANUFACTURER, ATTR_UNIT, + ATTR_UPTIME, ATTR_YELLOW_DRUM_COUNTER, ATTR_YELLOW_DRUM_REMAINING_LIFE, ATTR_YELLOW_DRUM_REMAINING_PAGES, @@ -76,8 +80,18 @@ class BrotherPrinterSensor(Entity): @property def state(self): """Return the state.""" + if self.kind == ATTR_UPTIME: + uptime = utcnow() - timedelta(seconds=self.coordinator.data.get(self.kind)) + return uptime.replace(microsecond=0).isoformat() return self.coordinator.data.get(self.kind) + @property + def device_class(self): + """Return the class of this sensor.""" + if self.kind == ATTR_UPTIME: + return DEVICE_CLASS_TIMESTAMP + return None + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/requirements_all.txt b/requirements_all.txt index 714e0eaf028..915ced918f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -384,7 +384,7 @@ bravia-tv==1.0.6 broadlink==0.14.1 # homeassistant.components.brother -brother==0.1.14 +brother==0.1.15 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5e01ba8c77..f4474cb79c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -205,7 +205,7 @@ bravia-tv==1.0.6 broadlink==0.14.1 # homeassistant.components.brother -brother==0.1.14 +brother==0.1.15 # homeassistant.components.bsblan bsblan==0.3.7 diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index aeff5a3697d..91109189483 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -1,18 +1,19 @@ """Test sensor of Brother integration.""" -from datetime import timedelta +from datetime import datetime, timedelta import json from homeassistant.components.brother.const import UNIT_PAGES from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_TIMESTAMP, STATE_UNAVAILABLE, - TIME_DAYS, UNIT_PERCENTAGE, ) from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow +from homeassistant.util.dt import UTC, utcnow from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture @@ -24,7 +25,12 @@ ATTR_COUNTER = "counter" async def test_sensors(hass): """Test states of the sensors.""" - await init_integration(hass) + test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC) + with patch( + "homeassistant.components.brother.sensor.utcnow", return_value=test_time + ): + await init_integration(hass) + registry = await hass.helpers.entity_registry.async_get_registry() state = hass.states.get("sensor.hl_l2340dw_status") @@ -208,9 +214,10 @@ async def test_sensors(hass): state = hass.states.get("sensor.hl_l2340dw_uptime") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:timer-outline" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_DAYS - assert state.state == "48" + assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.state == "2019-09-24T12:14:56+00:00" entry = registry.async_get("sensor.hl_l2340dw_uptime") assert entry From 4ff376cdd612f0355db5095de115c2782dc186cd Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 26 Aug 2020 04:28:30 -0500 Subject: [PATCH 354/862] Add timestamp option for input_datetime.set_datetime (#39121) --- .../components/input_datetime/__init__.py | 78 +++++++++++-------- .../components/input_datetime/services.yaml | 11 ++- tests/components/input_datetime/test_init.py | 48 +++++++++++- 3 files changed, 96 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index d000f606c58..e95287d2cbe 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -37,8 +37,34 @@ DEFAULT_DATE = datetime.date(1970, 1, 1) DEFAULT_TIME = datetime.time(0, 0, 0) ATTR_DATETIME = "datetime" +ATTR_TIMESTAMP = "timestamp" + + +def validate_set_datetime_attrs(config): + """Validate set_datetime service attributes.""" + has_date_or_time_attr = any(key in config for key in (ATTR_DATE, ATTR_TIME)) + if ( + sum([has_date_or_time_attr, ATTR_DATETIME in config, ATTR_TIMESTAMP in config]) + > 1 + ): + raise vol.Invalid(f"Cannot use together: {', '.join(config.keys())}") + return config + SERVICE_SET_DATETIME = "set_datetime" +SERVICE_SET_DATETIME_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(ATTR_DATE): cv.date, + vol.Optional(ATTR_TIME): cv.time, + vol.Optional(ATTR_DATETIME): cv.datetime, + vol.Optional(ATTR_TIMESTAMP): vol.Coerce(float), + }, + extra=vol.ALLOW_EXTRA, + ), + cv.has_at_least_one_key(ATTR_DATE, ATTR_TIME, ATTR_DATETIME, ATTR_TIMESTAMP), + validate_set_datetime_attrs, +) STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -138,37 +164,29 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_set_datetime_service(entity, call): """Handle a call to the input datetime 'set datetime' service.""" - time = call.data.get(ATTR_TIME) date = call.data.get(ATTR_DATE) + time = call.data.get(ATTR_TIME) dttm = call.data.get(ATTR_DATETIME) - if ( - dttm - and (date or time) - or entity.has_date - and not (date or dttm) - or entity.has_time - and not (time or dttm) - ): - _LOGGER.error( - "Invalid service data for %s input_datetime.set_datetime: %s", - entity.entity_id, - str(call.data), - ) - return + tmsp = call.data.get(ATTR_TIMESTAMP) + if tmsp: + dttm = dt_util.as_local(dt_util.utc_from_timestamp(tmsp)).replace( + tzinfo=None + ) if dttm: date = dttm.date() time = dttm.time() + if not entity.has_date: + date = None + if not entity.has_time: + time = None + if not date and not time: + raise vol.Invalid("Nothing to set") + entity.async_set_datetime(date, time) component.async_register_entity_service( - SERVICE_SET_DATETIME, - { - vol.Optional(ATTR_DATE): cv.date, - vol.Optional(ATTR_TIME): cv.time, - vol.Optional(ATTR_DATETIME): cv.datetime, - }, - async_set_datetime_service, + SERVICE_SET_DATETIME, SERVICE_SET_DATETIME_SCHEMA, async_set_datetime_service ) return True @@ -338,17 +356,11 @@ class InputDatetime(RestoreEntity): @callback def async_set_datetime(self, date_val, time_val): """Set a new date / time.""" - if self.has_date and self.has_time and date_val and time_val: - self._current_datetime = datetime.datetime.combine(date_val, time_val) - elif self.has_date and not self.has_time and date_val: - self._current_datetime = datetime.datetime.combine( - date_val, self._current_datetime.time() - ) - if self.has_time and not self.has_date and time_val: - self._current_datetime = datetime.datetime.combine( - self._current_datetime.date(), time_val - ) - + if not date_val: + date_val = self._current_datetime.date() + if not time_val: + time_val = self._current_datetime.time() + self._current_datetime = datetime.datetime.combine(date_val, time_val) self.async_write_ha_state() async def async_update_config(self, config: typing.Dict) -> None: diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index 26b2d088aea..bcbadc45aad 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -1,18 +1,21 @@ set_datetime: - description: This can be used to dynamically set the date and/or time. + description: This can be used to dynamically set the date and/or time. Use date/time, datetime or timestamp. fields: entity_id: description: Entity id of the input datetime to set the new value. example: input_datetime.test_date_time date: - description: The target date the entity should be set to. Do not use with datetime. + description: The target date the entity should be set to. example: '"2019-04-20"' time: - description: The target time the entity should be set to. Do not use with datetime. + description: The target time the entity should be set to. example: '"05:04:20"' datetime: - description: The target date & time the entity should be set to. Do not use with date or time. + description: The target date & time the entity should be set to. example: '"2019-04-20 05:04:20"' + timestamp: + description: The target date & time the entity should be set to as expressed by a UNIX timestamp. + example: 1598027400 reload: description: Reload the input_datetime configuration. diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 0eb4d748563..70f0b69d3ef 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components.input_datetime import ( ATTR_DATETIME, ATTR_EDITABLE, ATTR_TIME, + ATTR_TIMESTAMP, CONF_HAS_DATE, CONF_HAS_TIME, CONF_ID, @@ -25,6 +26,7 @@ from homeassistant.core import Context, CoreState, State from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from tests.async_mock import patch from tests.common import mock_restore_cache @@ -92,6 +94,16 @@ async def async_set_datetime(hass, entity_id, dt_value): ) +async def async_set_timestamp(hass, entity_id, timestamp): + """Set date and / or time of input_datetime.""" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DATETIME, + {ATTR_ENTITY_ID: entity_id, ATTR_TIMESTAMP: timestamp}, + blocking=True, + ) + + async def test_invalid_configs(hass): """Test config.""" invalid_configs = [ @@ -156,6 +168,32 @@ async def test_set_datetime_2(hass): assert state.attributes["timestamp"] == dt_obj.timestamp() +async def test_set_datetime_3(hass): + """Test set_datetime method using timestamp.""" + await async_setup_component( + hass, DOMAIN, {DOMAIN: {"test_datetime": {"has_time": True, "has_date": True}}} + ) + + entity_id = "input_datetime.test_datetime" + + dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30) + + await async_set_timestamp(hass, entity_id, dt_util.as_utc(dt_obj).timestamp()) + + state = hass.states.get(entity_id) + assert state.state == str(dt_obj) + assert state.attributes["has_time"] + assert state.attributes["has_date"] + + assert state.attributes["year"] == 2017 + assert state.attributes["month"] == 9 + assert state.attributes["day"] == 7 + assert state.attributes["hour"] == 19 + assert state.attributes["minute"] == 46 + assert state.attributes["second"] == 30 + assert state.attributes["timestamp"] == dt_obj.timestamp() + + async def test_set_datetime_time(hass): """Test set_datetime method with only time.""" await async_setup_component( @@ -199,7 +237,8 @@ async def test_set_invalid(hass): await hass.services.async_call( "input_datetime", "set_datetime", - {"entity_id": "test_date", "time": time_portion}, + {"entity_id": entity_id, "time": time_portion}, + blocking=True, ) await hass.async_block_till_done() @@ -229,7 +268,8 @@ async def test_set_invalid_2(hass): await hass.services.async_call( "input_datetime", "set_datetime", - {"entity_id": "test_date", "time": time_portion, "datetime": dt_obj}, + {"entity_id": entity_id, "time": time_portion, "datetime": dt_obj}, + blocking=True, ) await hass.async_block_till_done() @@ -358,8 +398,8 @@ async def test_input_datetime_context(hass, hass_admin_user): "input_datetime", "set_datetime", {"entity_id": state.entity_id, "date": "2018-01-02"}, - True, - Context(user_id=hass_admin_user.id), + blocking=True, + context=Context(user_id=hass_admin_user.id), ) state2 = hass.states.get("input_datetime.only_date") From 44118d8fb65255a4e76006149ed3f7f2e4b8bd59 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Aug 2020 13:40:31 +0200 Subject: [PATCH 355/862] Add cache version to GitHub Actions CI (#39277) --- .github/workflows/ci.yaml | 125 +++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 56 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e337c019f52..9b5aca3051c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,6 +10,7 @@ on: pull_request: ~ env: + CACHE_VERSION: 1 DEFAULT_PYTHON: 3.7 PRE_COMMIT_HOME: ~/.cache/pre-commit @@ -33,14 +34,15 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} restore-keys: | - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }} - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -58,9 +60,9 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} restore-keys: | - ${{ runner.os }}-pre-commit- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -85,8 +87,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -100,7 +103,7 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -129,8 +132,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -144,7 +148,7 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -173,8 +177,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -188,7 +193,7 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -239,8 +244,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -254,7 +260,7 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -286,8 +292,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -301,7 +308,7 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -333,8 +340,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -348,7 +356,7 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -377,8 +385,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -392,7 +401,7 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -424,8 +433,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -439,7 +449,7 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -479,8 +489,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -494,7 +505,7 @@ jobs: with: path: ${{ env.PRE_COMMIT_HOME }} key: | - ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - name: Fail job if cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -526,8 +537,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -558,8 +570,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version - }}-${{ hashFiles('requirements.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ + steps.python.outputs.python-version }}-${{ + hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed @@ -589,14 +602,14 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('requirements_all.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ + matrix.python-version }}-${{ hashFiles('requirements_test.txt') + }}-${{ hashFiles('requirements_all.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} restore-keys: | - ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }} - ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }} - ${{ runner.os }}-venv-${{ matrix.python-version }}- + ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }} + ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}- - name: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' @@ -630,9 +643,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('requirements_all.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ + matrix.python-version }}-${{ hashFiles('requirements_test.txt') + }}-${{ hashFiles('requirements_all.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -665,9 +678,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('requirements_all.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ + matrix.python-version }}-${{ hashFiles('requirements_test.txt') + }}-${{ hashFiles('requirements_all.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -702,9 +715,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('requirements_all.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ + matrix.python-version }}-${{ hashFiles('requirements_test.txt') + }}-${{ hashFiles('requirements_all.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' @@ -763,9 +776,9 @@ jobs: with: path: venv key: >- - ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('requirements_all.txt') }}-${{ + ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ + matrix.python-version }}-${{ hashFiles('requirements_test.txt') + }}-${{ hashFiles('requirements_all.txt') }}-${{ hashFiles('homeassistant/package_constraints.txt') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' From df9de8eb5d23b4c4325ced08354265feb4646aeb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Aug 2020 14:50:14 +0200 Subject: [PATCH 356/862] Prevent bluepy from being part of requirements_all.txt (#39275) --- requirements_all.txt | 4 ++-- script/gen_requirements_all.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 915ced918f3..87dcc0d5e15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -298,7 +298,7 @@ aurorapy==0.2.6 av==8.0.2 # homeassistant.components.avea -avea==1.4 +# avea==1.4 # homeassistant.components.avion # avion==0.10 @@ -334,7 +334,7 @@ batinfo==0.4.2 beautifulsoup4==4.9.1 # homeassistant.components.beewi_smartclim -beewi_smartclim==0.0.7 +# beewi_smartclim==0.0.7 # homeassistant.components.zha bellows==0.18.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 25d4d3d3e7e..ef70852277c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -15,8 +15,10 @@ from homeassistant.util.yaml.loader import load_yaml COMMENT_REQUIREMENTS = ( "Adafruit_BBIO", "Adafruit-DHT", + "avea", # depends on bluepy "avion", "beacontools", + "beewi_smartclim", # depends on bluepy "blinkt", "bluepy", "bme680", From e96d8a961c11bb4be327899cc19274f2702731d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Aug 2020 14:51:41 +0200 Subject: [PATCH 357/862] Block typing from being installed (#37707) --- .github/workflows/ci.yaml | 26 +++++-------------- Dockerfile | 1 - azure-pipelines-ci.yml | 6 ----- .../components/hdmi_cec/manifest.json | 1 + .../components/miflora/manifest.json | 1 + homeassistant/package_constraints.txt | 8 +++--- requirements_all.txt | 7 ----- script/gen_requirements_all.py | 8 +++--- 8 files changed, 16 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9b5aca3051c..fae621670ec 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -50,10 +50,6 @@ jobs: . venv/bin/activate pip install -U pip setuptools pip install -r requirements.txt -r requirements_test.txt - # Uninstalling typing as a workaround. Eventually we should make sure - # all our dependencies drop typing. - # Find offending deps with `pipdeptree -r -p typing` - pip uninstall -y typing - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache@v2 @@ -595,8 +591,7 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -610,8 +605,7 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }} ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }} ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}- - - name: - Create full Python ${{ matrix.python-version }} virtual environment + - name: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv @@ -619,10 +613,6 @@ jobs: pip install -U pip setuptools wheel pip install -r requirements_all.txt pip install -r requirements_test.txt - # Uninstalling typing as a workaround. Eventually we should make sure - # all our dependencies drop typing. - # Find offending deps with `pipdeptree -r -p typing` - pip uninstall -y typing pip install -e . pylint: @@ -636,8 +626,7 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -671,8 +660,7 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -708,8 +696,7 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -769,8 +756,7 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: diff --git a/Dockerfile b/Dockerfile index daaa2999127..cbcc948f5dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,6 @@ COPY . homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements_all.txt \ - && pip3 uninstall -y typing \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -e ./homeassistant \ && python3 -m compileall homeassistant/homeassistant diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 6b2975f1b20..613e386b249 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -166,9 +166,6 @@ stages: . venv/bin/activate pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt pip install -r requirements_test_all.txt - # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. - # Find offending deps with `pipdeptree -r -p typing` - pip uninstall -y typing - script: | . venv/bin/activate pip install -e . @@ -211,9 +208,6 @@ stages: pip install -U pip setuptools wheel pip install -r requirements_all.txt pip install -r requirements_test.txt - # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. - # Find offending deps with `pipdeptree -r -p typing` - pip uninstall -y typing - script: | . venv/bin/activate pip install -e . diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index 3d2ea355e02..c3817d25776 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -1,4 +1,5 @@ { + "disabled": "Dependency contains code that breaks Home Assistant.", "domain": "hdmi_cec", "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index fde97154194..96558c82fec 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -1,4 +1,5 @@ { + "disabled": "Dependency contains code that breaks Home Assistant.", "domain": "miflora", "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 261dd5dd34d..22ce4e772d2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,11 +39,11 @@ urllib3>=1.24.3 # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 -# Not needed for our supported Python versions -enum34==1000000000.0.0 - # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 -# This is built-in and breaks pip if installed +# This overrides a built-in Python package +enum34==1000000000.0.0 +typing==1000000000.0.0 uuid==1000000000.0.0 + diff --git a/requirements_all.txt b/requirements_all.txt index 87dcc0d5e15..c26e0322b17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -361,7 +361,6 @@ blinkstick==1.1.8 blockchain==1.4.4 # homeassistant.components.decora -# homeassistant.components.miflora # bluepy==1.3.0 # homeassistant.components.bme680 @@ -913,9 +912,6 @@ meteofrance-api==0.1.1 # homeassistant.components.mfi mficlient==0.3.0 -# homeassistant.components.miflora -miflora==0.6.0 - # homeassistant.components.mill millheater==0.3.4 @@ -1177,9 +1173,6 @@ py-synology==0.2.0 # homeassistant.components.seventeentrack py17track==2.2.2 -# homeassistant.components.hdmi_cec -pyCEC==0.4.13 - # homeassistant.components.control4 pyControl4==0.0.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ef70852277c..5947fd9ed5f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,14 +68,14 @@ urllib3>=1.24.3 # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 -# Not needed for our supported Python versions -enum34==1000000000.0.0 - # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 -# This is built-in and breaks pip if installed +# This overrides a built-in Python package +enum34==1000000000.0.0 +typing==1000000000.0.0 uuid==1000000000.0.0 + """ IGNORE_PRE_COMMIT_HOOK_ID = ( From dc84196202a4683881ddac9942fe1c9af6de3b5b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2020 07:52:19 -0500 Subject: [PATCH 358/862] Add the ability to reload statistics platforms from yaml (#39268) --- .../components/statistics/__init__.py | 3 + homeassistant/components/statistics/sensor.py | 12 +++- .../components/statistics/services.yaml | 2 + tests/components/statistics/test_sensor.py | 59 ++++++++++++++++++- tests/fixtures/statistics/configuration.yaml | 4 ++ 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/statistics/services.yaml create mode 100644 tests/fixtures/statistics/configuration.yaml diff --git a/homeassistant/components/statistics/__init__.py b/homeassistant/components/statistics/__init__.py index 3f0f03b4909..ce5f959d731 100644 --- a/homeassistant/components/statistics/__init__.py +++ b/homeassistant/components/statistics/__init__.py @@ -1 +1,4 @@ """The statistics component.""" + +DOMAIN = "statistics" +PLATFORMS = ["sensor"] diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 945e5ff89d1..11cddc88c87 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -23,8 +23,11 @@ from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change_event, ) +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.util import dt as dt_util +from . import DOMAIN, PLATFORMS + _LOGGER = logging.getLogger(__name__) ATTR_AVERAGE_CHANGE = "average_change" @@ -66,6 +69,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Statistics sensor.""" + + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + entity_id = config.get(CONF_ENTITY_ID) name = config.get(CONF_NAME) sampling_size = config.get(CONF_SAMPLING_SIZE) @@ -124,8 +130,10 @@ class StatisticsSensor(Entity): """Add listener and get recorded state.""" _LOGGER.debug("Startup for %s", self.entity_id) - async_track_state_change_event( - self.hass, [self._entity_id], async_stats_sensor_state_listener + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._entity_id], async_stats_sensor_state_listener + ) ) if "recorder" in self.hass.config.components: diff --git a/homeassistant/components/statistics/services.yaml b/homeassistant/components/statistics/services.yaml new file mode 100644 index 00000000000..608e2991334 --- /dev/null +++ b/homeassistant/components/statistics/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all statistics entities. diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 6d7f0963fa2..7a96ec4e2a4 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -1,14 +1,21 @@ """The test for the statistics sensor platform.""" from datetime import datetime, timedelta +from os import path import statistics import unittest import pytest +from homeassistant import config as hass_config from homeassistant.components import recorder -from homeassistant.components.statistics.sensor import StatisticsSensor -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS -from homeassistant.setup import setup_component +from homeassistant.components.statistics.sensor import DOMAIN, StatisticsSensor +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + SERVICE_RELOAD, + STATE_UNKNOWN, + TEMP_CELSIUS, +) +from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util from tests.async_mock import patch @@ -442,3 +449,49 @@ class TestStatisticsSensor(unittest.TestCase): assert mock_data["return_time"] == state.attributes.get("max_age") + timedelta( hours=1 ) + + +async def test_reload(hass): + """Verify we can reload filter sensors.""" + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db + + hass.states.async_set("sensor.test_monitored", 12345) + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "sampling_size": 100, + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("sensor.test") + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "statistics/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("sensor.test") is None + assert hass.states.get("sensor.cputest") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/statistics/configuration.yaml b/tests/fixtures/statistics/configuration.yaml new file mode 100644 index 00000000000..a6ce34377a0 --- /dev/null +++ b/tests/fixtures/statistics/configuration.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: statistics + entity_id: sensor.cpu + name: cputest From b47992dba08a8b72c2258cc5f33ec32c44b6caa4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Aug 2020 16:53:22 +0200 Subject: [PATCH 359/862] Bump CI cache (#39283) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 22ce4e772d2..7b73b9b9b00 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -42,6 +42,9 @@ httplib2>=0.18.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 +# To remove reliance on typing +btlewrap>=0.0.10 + # This overrides a built-in Python package enum34==1000000000.0.0 typing==1000000000.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5947fd9ed5f..07883dc6221 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -71,6 +71,9 @@ httplib2>=0.18.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 +# To remove reliance on typing +btlewrap>=0.0.10 + # This overrides a built-in Python package enum34==1000000000.0.0 typing==1000000000.0.0 From 51a63c1fc413670fb776560380c1ad24c3998297 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Aug 2020 16:57:52 +0200 Subject: [PATCH 360/862] Drop last bits of asyncio.coroutine (#39280) --- .../components/mqtt_eventstream/__init__.py | 6 +- tests/common.py | 3 +- tests/components/shell_command/test_init.py | 5 +- tests/test_core.py | 158 ++++++++---------- tests/util/test_async.py | 101 ----------- tests/util/test_package.py | 5 +- 6 files changed, 74 insertions(+), 204 deletions(-) diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index 48448355df8..5a5f3b3c74d 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -1,5 +1,4 @@ """Connect two Home Assistant instances via MQTT.""" -import asyncio import json import voluptuous as vol @@ -39,8 +38,7 @@ CONFIG_SCHEMA = vol.Schema( ) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the MQTT eventstream component.""" mqtt = hass.components.mqtt conf = config.get(DOMAIN, {}) @@ -103,6 +101,6 @@ def async_setup(hass, config): # Only subscribe if you specified a topic. if sub_topic: - yield from mqtt.async_subscribe(sub_topic, _event_receiver) + await mqtt.async_subscribe(sub_topic, _event_receiver) return True diff --git a/tests/common.py b/tests/common.py index e2e183061a7..1cba478767f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -261,8 +261,7 @@ def async_mock_intent(hass, intent_typ): class MockIntentHandler(intent.IntentHandler): intent_type = intent_typ - @asyncio.coroutine - def async_handle(self, intent): + async def async_handle(self, intent): """Handle the intent.""" intents.append(intent) return intent.create_response() diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 7019d22fac8..1f8e442e93d 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -12,11 +12,10 @@ from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant -def mock_process_creator(error: bool = False) -> asyncio.coroutine: +def mock_process_creator(error: bool = False): """Mock a coroutine that creates a process when yielded.""" - @asyncio.coroutine - def communicate() -> Tuple[bytes, bytes]: + async def communicate() -> Tuple[bytes, bytes]: """Mock a coroutine that runs a process when yielded. Returns a tuple of (stdout, stderr). diff --git a/tests/test_core.py b/tests/test_core.py index 05a969d0a75..22f1e779061 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -174,120 +174,98 @@ def test_stage_shutdown(): assert len(test_all) == 2 -class TestHomeAssistant(unittest.TestCase): - """Test the Home Assistant core classes.""" +async def test_pending_sheduler(hass): + """Add a coro to pending tasks.""" + call_count = [] - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() + async def test_coro(): + """Test Coro.""" + call_count.append("call") - # pylint: disable=invalid-name - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + for _ in range(3): + hass.async_add_job(test_coro()) - def test_pending_sheduler(self): - """Add a coro to pending tasks.""" - call_count = [] + await asyncio.wait(hass._pending_tasks) - @asyncio.coroutine - def test_coro(): - """Test Coro.""" - call_count.append("call") + assert len(hass._pending_tasks) == 3 + assert len(call_count) == 3 - for _ in range(3): - self.hass.add_job(test_coro()) - asyncio.run_coroutine_threadsafe( - asyncio.wait(self.hass._pending_tasks), loop=self.hass.loop - ).result() +async def test_async_add_job_pending_tasks_coro(hass): + """Add a coro to pending tasks.""" + call_count = [] - assert len(self.hass._pending_tasks) == 3 - assert len(call_count) == 3 + async def test_coro(): + """Test Coro.""" + call_count.append("call") - def test_async_add_job_pending_tasks_coro(self): - """Add a coro to pending tasks.""" - call_count = [] + for _ in range(2): + hass.add_job(test_coro()) - @asyncio.coroutine - def test_coro(): - """Test Coro.""" - call_count.append("call") + async def wait_finish_callback(): + """Wait until all stuff is scheduled.""" + await asyncio.sleep(0) + await asyncio.sleep(0) - for _ in range(2): - self.hass.add_job(test_coro()) + await wait_finish_callback() - @asyncio.coroutine - def wait_finish_callback(): - """Wait until all stuff is scheduled.""" - yield from asyncio.sleep(0) - yield from asyncio.sleep(0) + assert len(hass._pending_tasks) == 2 + await hass.async_block_till_done() + assert len(call_count) == 2 - asyncio.run_coroutine_threadsafe( - wait_finish_callback(), self.hass.loop - ).result() - assert len(self.hass._pending_tasks) == 2 - self.hass.block_till_done() - assert len(call_count) == 2 +async def test_async_add_job_pending_tasks_executor(hass): + """Run an executor in pending tasks.""" + call_count = [] - def test_async_add_job_pending_tasks_executor(self): - """Run an executor in pending tasks.""" - call_count = [] + def test_executor(): + """Test executor.""" + call_count.append("call") - def test_executor(): - """Test executor.""" - call_count.append("call") + async def wait_finish_callback(): + """Wait until all stuff is scheduled.""" + await asyncio.sleep(0) + await asyncio.sleep(0) - @asyncio.coroutine - def wait_finish_callback(): - """Wait until all stuff is scheduled.""" - yield from asyncio.sleep(0) - yield from asyncio.sleep(0) + for _ in range(2): + hass.async_add_job(test_executor) - for _ in range(2): - self.hass.add_job(test_executor) + await wait_finish_callback() - asyncio.run_coroutine_threadsafe( - wait_finish_callback(), self.hass.loop - ).result() + assert len(hass._pending_tasks) == 2 + await hass.async_block_till_done() + assert len(call_count) == 2 - assert len(self.hass._pending_tasks) == 2 - self.hass.block_till_done() - assert len(call_count) == 2 - def test_async_add_job_pending_tasks_callback(self): - """Run a callback in pending tasks.""" - call_count = [] +async def test_async_add_job_pending_tasks_callback(hass): + """Run a callback in pending tasks.""" + call_count = [] - @ha.callback - def test_callback(): - """Test callback.""" - call_count.append("call") + @ha.callback + def test_callback(): + """Test callback.""" + call_count.append("call") - @asyncio.coroutine - def wait_finish_callback(): - """Wait until all stuff is scheduled.""" - yield from asyncio.sleep(0) - yield from asyncio.sleep(0) + async def wait_finish_callback(): + """Wait until all stuff is scheduled.""" + await asyncio.sleep(0) + await asyncio.sleep(0) - for _ in range(2): - self.hass.add_job(test_callback) + for _ in range(2): + hass.async_add_job(test_callback) - asyncio.run_coroutine_threadsafe( - wait_finish_callback(), self.hass.loop - ).result() + await wait_finish_callback() - self.hass.block_till_done() + await hass.async_block_till_done() - assert len(self.hass._pending_tasks) == 0 - assert len(call_count) == 2 + assert len(hass._pending_tasks) == 0 + assert len(call_count) == 2 - def test_add_job_with_none(self): - """Try to add a job with None as function.""" - with pytest.raises(ValueError): - self.hass.add_job(None, "test_arg") + +async def test_add_job_with_none(hass): + """Try to add a job with None as function.""" + with pytest.raises(ValueError): + hass.async_add_job(None, "test_arg") class TestEvent(unittest.TestCase): @@ -412,8 +390,7 @@ class TestEventBus(unittest.TestCase): """Test listen_once_event method.""" runs = [] - @asyncio.coroutine - def event_handler(event): + async def event_handler(event): runs.append(event) self.bus.listen_once("test_event", event_handler) @@ -470,8 +447,7 @@ class TestEventBus(unittest.TestCase): """Test coroutine event listener.""" coroutine_calls = [] - @asyncio.coroutine - def coroutine_listener(event): + async def coroutine_listener(event): coroutine_calls.append(event) self.bus.listen("test_coroutine", coroutine_listener) diff --git a/tests/util/test_async.py b/tests/util/test_async.py index b60b4097c52..460490eb783 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,7 +1,4 @@ """Tests for async util methods from Python source.""" -import asyncio -from unittest import TestCase - import pytest from homeassistant.util import async_ as hasync @@ -70,104 +67,6 @@ def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _): assert len(loop.call_soon_threadsafe.mock_calls) == 2 -class RunThreadsafeTests(TestCase): - """Test case for hasync.run_coroutine_threadsafe.""" - - def setUp(self): - """Test setup method.""" - self.loop = asyncio.new_event_loop() - - def tearDown(self): - """Test teardown method.""" - executor = self.loop._default_executor - if executor is not None: - executor.shutdown(wait=True) - self.loop.close() - - @staticmethod - def run_briefly(loop): - """Momentarily run a coroutine on the given loop.""" - - @asyncio.coroutine - def once(): - pass - - gen = once() - t = loop.create_task(gen) - try: - loop.run_until_complete(t) - finally: - gen.close() - - def add_callback(self, a, b, fail, invalid): - """Return a + b.""" - if fail: - raise RuntimeError("Fail!") - if invalid: - raise ValueError("Invalid!") - return a + b - - @asyncio.coroutine - def add_coroutine(self, a, b, fail, invalid, cancel): - """Wait 0.05 second and return a + b.""" - yield from asyncio.sleep(0.05, loop=self.loop) - if cancel: - asyncio.current_task(self.loop).cancel() - yield - return self.add_callback(a, b, fail, invalid) - - def target_callback(self, fail=False, invalid=False): - """Run add callback in the event loop.""" - future = hasync.run_callback_threadsafe( - self.loop, self.add_callback, 1, 2, fail, invalid - ) - try: - return future.result() - finally: - future.done() or future.cancel() - - def target_coroutine( - self, fail=False, invalid=False, cancel=False, timeout=None, advance_coro=False - ): - """Run add coroutine in the event loop.""" - coro = self.add_coroutine(1, 2, fail, invalid, cancel) - future = hasync.run_coroutine_threadsafe(coro, self.loop) - if advance_coro: - # this is for test_run_coroutine_threadsafe_task_factory_exception; - # otherwise it spills errors and breaks **other** unittests, since - # 'target_coroutine' is interacting with threads. - - # With this call, `coro` will be advanced, so that - # CoroWrapper.__del__ won't do anything when asyncio tests run - # in debug mode. - self.loop.call_soon_threadsafe(coro.send, None) - try: - return future.result(timeout) - finally: - future.done() or future.cancel() - - def test_run_callback_threadsafe(self): - """Test callback submission from a thread to an event loop.""" - future = self.loop.run_in_executor(None, self.target_callback) - result = self.loop.run_until_complete(future) - self.assertEqual(result, 3) - - def test_run_callback_threadsafe_with_exception(self): - """Test callback submission from thread to event loop on exception.""" - future = self.loop.run_in_executor(None, self.target_callback, True) - with self.assertRaises(RuntimeError) as exc_context: - self.loop.run_until_complete(future) - self.assertIn("Fail!", exc_context.exception.args) - - def test_run_callback_threadsafe_with_invalid(self): - """Test callback submission from thread to event loop on invalid.""" - callback = lambda: self.target_callback(invalid=True) # noqa: E731 - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(ValueError) as exc_context: - self.loop.run_until_complete(future) - self.assertIn("Invalid!", exc_context.exception.args) - - async def test_check_loop_async(): """Test check_loop detects when called from event loop without integration context.""" with pytest.raises(RuntimeError): diff --git a/tests/util/test_package.py b/tests/util/test_package.py index c9f5183249e..064d1379e08 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -216,8 +216,7 @@ def test_install_find_links(mock_sys, mock_popen, mock_env_copy, mock_venv): assert mock_popen.return_value.communicate.call_count == 1 -@asyncio.coroutine -def test_async_get_user_site(mock_env_copy): +async def test_async_get_user_site(mock_env_copy): """Test async get user site directory.""" deps_dir = "/deps_dir" env = mock_env_copy() @@ -227,7 +226,7 @@ def test_async_get_user_site(mock_env_copy): "homeassistant.util.package.asyncio.create_subprocess_exec", return_value=mock_async_subprocess(), ) as popen_mock: - ret = yield from package.async_get_user_site(deps_dir) + ret = await package.async_get_user_site(deps_dir) assert popen_mock.call_count == 1 assert popen_mock.call_args == call( *args, From 715fe4eef8fca75a391f245ce7dab0beaa928b3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2020 10:50:25 -0500 Subject: [PATCH 361/862] Fix time pattern listener firing a few microseconds early (#39281) --- homeassistant/helpers/event.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index ba0b07a207e..cd436919997 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -996,13 +996,19 @@ def async_track_utc_time_change( calculate_next(now + timedelta(seconds=1)) + # We always get time.time() first to avoid time.time() + # ticking forward after fetching hass.loop.time() + # and callback being scheduled a few microseconds early cancel_callback = hass.loop.call_at( - hass.loop.time() + next_time.timestamp() - time.time(), + -time.time() + hass.loop.time() + next_time.timestamp(), pattern_time_change_listener, ) + # We always get time.time() first to avoid time.time() + # ticking forward after fetching hass.loop.time() + # and callback being scheduled a few microseconds early cancel_callback = hass.loop.call_at( - hass.loop.time() + next_time.timestamp() - time.time(), + -time.time() + hass.loop.time() + next_time.timestamp(), pattern_time_change_listener, ) From 79f4b6eb6bf13d9c7dee6c97203d33857ec98d14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2020 10:53:11 -0500 Subject: [PATCH 362/862] Cleanup the rest reload test to use the pytest requests_mock fixture (#39282) --- tests/components/rest/test_sensor.py | 41 +++++++++++++--------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 39197680887..ef4ba7a2b55 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -681,26 +681,26 @@ class TestRestData(unittest.TestCase): assert self.rest.data is None -async def test_reload(hass): +async def test_reload(hass, requests_mock): """Verify we can reload reset sensors.""" - with requests_mock.Mocker() as mock_req: - mock_req.get("http://localhost", text="test data") - await async_setup_component( - hass, - "sensor", - { - "sensor": { - "platform": "rest", - "method": "GET", - "name": "mockrest", - "resource": "http://localhost", - } - }, - ) - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() + requests_mock.get("http://localhost", text="test data") + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "rest", + "method": "GET", + "name": "mockrest", + "resource": "http://localhost", + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 @@ -709,10 +709,7 @@ async def test_reload(hass): yaml_path = path.join( _get_fixtures_base_path(), "fixtures", "rest/configuration.yaml", ) - with patch.object( - hass_config, "YAML_CONFIG_FILE", yaml_path - ), requests_mock.Mocker() as mock_req: - mock_req.get("http://localhost", text="test data 2") + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( "rest", SERVICE_RELOAD, {}, blocking=True, ) From a2651845f379992231fd7b9c8458828036296ee0 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 26 Aug 2020 18:03:03 +0200 Subject: [PATCH 363/862] Centralize knx config and update xknx to 0.12.0 (#39219) * Refactor KNX integration to centralize configuration yaml (#39189) * Updates for xknx 0.12.0 (#38880) --- CODEOWNERS | 2 +- homeassistant/components/knx/__init__.py | 163 +++++---- homeassistant/components/knx/binary_sensor.py | 88 +---- homeassistant/components/knx/climate.py | 186 +--------- homeassistant/components/knx/const.py | 61 ++++ homeassistant/components/knx/cover.py | 124 +++---- homeassistant/components/knx/factory.py | 263 ++++++++++++++ homeassistant/components/knx/light.py | 96 +---- homeassistant/components/knx/manifest.json | 4 +- homeassistant/components/knx/notify.py | 33 +- homeassistant/components/knx/scene.py | 34 +- homeassistant/components/knx/schema.py | 342 ++++++++++++++++++ homeassistant/components/knx/sensor.py | 35 +- homeassistant/components/knx/services.yaml | 3 + homeassistant/components/knx/switch.py | 33 +- requirements_all.txt | 2 +- 16 files changed, 847 insertions(+), 622 deletions(-) create mode 100644 homeassistant/components/knx/const.py create mode 100644 homeassistant/components/knx/factory.py create mode 100644 homeassistant/components/knx/schema.py diff --git a/CODEOWNERS b/CODEOWNERS index 84869fe7144..41914c61c67 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -223,7 +223,7 @@ homeassistant/components/keba/* @dannerph homeassistant/components/keenetic_ndms2/* @foxel homeassistant/components/kef/* @basnijholt homeassistant/components/keyboard_remote/* @bendavid -homeassistant/components/knx/* @Julius2342 +homeassistant/components/knx/* @Julius2342 @farmio @marvin-w homeassistant/components/kodi/* @armills @OnFreund homeassistant/components/konnected/* @heythisisnate @kit-klein homeassistant/components/lametric/* @robbiet480 diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index d5bfdcc0e57..4de801a19d1 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -3,8 +3,8 @@ import logging import voluptuous as vol from xknx import XKNX -from xknx.devices import ActionCallback, DateTime, DateTimeBroadcastType, ExposeSensor -from xknx.dpt import DPTArray, DPTBinary +from xknx.devices import DateTime, ExposeSensor +from xknx.dpt import DPTArray, DPTBase, DPTBinary from xknx.exceptions import XKNXException from xknx.io import DEFAULT_MCAST_PORT, ConnectionConfig, ConnectionType from xknx.telegram import AddressFilter, GroupAddress, Telegram @@ -23,60 +23,61 @@ from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.script import Script + +from .const import DATA_KNX, DOMAIN, DeviceTypes +from .factory import create_knx_device +from .schema import ( + BinarySensorSchema, + ClimateSchema, + ConnectionSchema, + CoverSchema, + ExposeSchema, + LightSchema, + NotifySchema, + SceneSchema, + SensorSchema, + SwitchSchema, +) _LOGGER = logging.getLogger(__name__) -DOMAIN = "knx" -DATA_KNX = "data_knx" CONF_KNX_CONFIG = "config_file" CONF_KNX_ROUTING = "routing" CONF_KNX_TUNNELING = "tunneling" -CONF_KNX_LOCAL_IP = "local_ip" CONF_KNX_FIRE_EVENT = "fire_event" CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter" CONF_KNX_STATE_UPDATER = "state_updater" CONF_KNX_RATE_LIMIT = "rate_limit" CONF_KNX_EXPOSE = "expose" -CONF_KNX_EXPOSE_TYPE = "type" -CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" -CONF_KNX_EXPOSE_DEFAULT = "default" -CONF_KNX_EXPOSE_ADDRESS = "address" + +CONF_KNX_LIGHT = "light" +CONF_KNX_COVER = "cover" +CONF_KNX_BINARY_SENSOR = "binary_sensor" +CONF_KNX_SCENE = "scene" +CONF_KNX_SENSOR = "sensor" +CONF_KNX_SWITCH = "switch" +CONF_KNX_NOTIFY = "notify" +CONF_KNX_CLIMATE = "climate" SERVICE_KNX_SEND = "send" SERVICE_KNX_ATTR_ADDRESS = "address" SERVICE_KNX_ATTR_PAYLOAD = "payload" +SERVICE_KNX_ATTR_TYPE = "type" ATTR_DISCOVER_DEVICES = "devices" -TUNNELING_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_KNX_LOCAL_IP): cv.string, - vol.Optional(CONF_PORT): cv.port, - } -) - -ROUTING_SCHEMA = vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string}) - -EXPOSE_SCHEMA = vol.Schema( - { - vol.Required(CONF_KNX_EXPOSE_TYPE): cv.string, - vol.Optional(CONF_ENTITY_ID): cv.entity_id, - vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, - vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, - vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string, - } -) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Optional(CONF_KNX_CONFIG): cv.string, - vol.Exclusive(CONF_KNX_ROUTING, "connection_type"): ROUTING_SCHEMA, - vol.Exclusive(CONF_KNX_TUNNELING, "connection_type"): TUNNELING_SCHEMA, + vol.Exclusive( + CONF_KNX_ROUTING, "connection_type" + ): ConnectionSchema.ROUTING_SCHEMA, + vol.Exclusive( + CONF_KNX_TUNNELING, "connection_type" + ): ConnectionSchema.TUNNELING_SCHEMA, vol.Inclusive(CONF_KNX_FIRE_EVENT, "fire_ev"): cv.boolean, vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, "fire_ev"): vol.All( cv.ensure_list, [cv.string] @@ -85,7 +86,33 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All( vol.Coerce(int), vol.Range(min=1, max=100) ), - vol.Optional(CONF_KNX_EXPOSE): vol.All(cv.ensure_list, [EXPOSE_SCHEMA]), + vol.Optional(CONF_KNX_EXPOSE): vol.All( + cv.ensure_list, [ExposeSchema.SCHEMA] + ), + vol.Optional(CONF_KNX_COVER): vol.All( + cv.ensure_list, [CoverSchema.SCHEMA] + ), + vol.Optional(CONF_KNX_BINARY_SENSOR): vol.All( + cv.ensure_list, [BinarySensorSchema.SCHEMA] + ), + vol.Optional(CONF_KNX_LIGHT): vol.All( + cv.ensure_list, [LightSchema.SCHEMA] + ), + vol.Optional(CONF_KNX_CLIMATE): vol.All( + cv.ensure_list, [ClimateSchema.SCHEMA] + ), + vol.Optional(CONF_KNX_NOTIFY): vol.All( + cv.ensure_list, [NotifySchema.SCHEMA] + ), + vol.Optional(CONF_KNX_SWITCH): vol.All( + cv.ensure_list, [SwitchSchema.SCHEMA] + ), + vol.Optional(CONF_KNX_SENSOR): vol.All( + cv.ensure_list, [SensorSchema.SCHEMA] + ), + vol.Optional(CONF_KNX_SCENE): vol.All( + cv.ensure_list, [SceneSchema.SCHEMA] + ), } ) }, @@ -98,9 +125,21 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema( vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( cv.positive_int, [cv.positive_int] ), + vol.Optional(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), } ) +KNX_CONFIG_PLATFORM_MAPPING = { + CONF_KNX_COVER: DeviceTypes.cover, + CONF_KNX_SWITCH: DeviceTypes.switch, + CONF_KNX_LIGHT: DeviceTypes.light, + CONF_KNX_SENSOR: DeviceTypes.sensor, + CONF_KNX_NOTIFY: DeviceTypes.notify, + CONF_KNX_SCENE: DeviceTypes.scene, + CONF_KNX_BINARY_SENSOR: DeviceTypes.binary_sensor, + CONF_KNX_CLIMATE: DeviceTypes.climate, +} + async def async_setup(hass, config): """Set up the KNX component.""" @@ -114,6 +153,15 @@ async def async_setup(hass, config): f"Can't connect to KNX interface:
{ex}", title="KNX" ) + for platform_config, device_type in KNX_CONFIG_PLATFORM_MAPPING.items(): + if platform_config in config[DOMAIN]: + for device_config in config[DOMAIN][platform_config]: + hass.data[DATA_KNX].xknx.devices.add( + create_knx_device( + hass, device_type, hass.data[DATA_KNX].xknx, device_config + ) + ) + for component, discovery_type in ( ("switch", "Switch"), ("climate", "Climate"), @@ -203,11 +251,15 @@ class KNXModule: return self.connection_config_tunneling() if CONF_KNX_ROUTING in self.config[DOMAIN]: return self.connection_config_routing() - return self.connection_config_auto() + # return None to let xknx use config from xknx.yaml connection block if given + # otherwise it will use default ConnectionConfig (Automatic) + return None def connection_config_routing(self): """Return the connection_config if routing is configured.""" - local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP) + local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ) return ConnectionConfig( connection_type=ConnectionType.ROUTING, local_ip=local_ip ) @@ -216,7 +268,9 @@ class KNXModule: """Return the connection_config if tunneling is configured.""" gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_HOST] gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT) - local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP) + local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get( + ConnectionSchema.CONF_KNX_LOCAL_IP + ) if gateway_port is None: gateway_port = DEFAULT_MCAST_PORT return ConnectionConfig( @@ -227,11 +281,6 @@ class KNXModule: auto_reconnect=True, ) - def connection_config_auto(self): - """Return the connection_config if auto is configured.""" - # pylint: disable=no-self-use - return ConnectionConfig() - def register_callbacks(self): """Register callbacks within XKNX object.""" if ( @@ -251,11 +300,11 @@ class KNXModule: if CONF_KNX_EXPOSE not in self.config[DOMAIN]: return for to_expose in self.config[DOMAIN][CONF_KNX_EXPOSE]: - expose_type = to_expose.get(CONF_KNX_EXPOSE_TYPE) + expose_type = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_TYPE) entity_id = to_expose.get(CONF_ENTITY_ID) - attribute = to_expose.get(CONF_KNX_EXPOSE_ATTRIBUTE) - default = to_expose.get(CONF_KNX_EXPOSE_DEFAULT) - address = to_expose.get(CONF_KNX_EXPOSE_ADDRESS) + attribute = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE) + default = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT) + address = to_expose.get(ExposeSchema.CONF_KNX_EXPOSE_ADDRESS) if expose_type in ["time", "date", "datetime"]: exposure = KNXExposeTime(self.xknx, expose_type, address) exposure.async_register() @@ -286,9 +335,15 @@ class KNXModule: """Service for sending an arbitrary KNX message to the KNX bus.""" attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS) + attr_type = call.data.get(SERVICE_KNX_ATTR_TYPE) def calculate_payload(attr_payload): """Calculate payload depending on type of attribute.""" + if attr_type is not None: + transcoder = DPTBase.parse_transcoder(attr_type) + if transcoder is None: + raise ValueError(f"Invalid type for knx.send service: {attr_type}") + return DPTArray(transcoder.to_knx(attr_payload)) if isinstance(attr_payload, int): return DPTBinary(attr_payload) return DPTArray(attr_payload) @@ -302,22 +357,6 @@ class KNXModule: await self.xknx.telegrams.put(telegram) -class KNXAutomation: - """Wrapper around xknx.devices.ActionCallback object..""" - - def __init__(self, hass, device, hook, action, counter=1): - """Initialize Automation class.""" - self.hass = hass - self.device = device - script_name = f"{device.get_name()} turn ON script" - self.script = Script(hass, action, script_name, DOMAIN) - - self.action = ActionCallback( - hass.data[DATA_KNX].xknx, self.script.async_run, hook=hook, counter=counter - ) - device.actions.append(self.action) - - class KNXExposeTime: """Object to Expose Time/Date object to KNX bus.""" @@ -332,7 +371,7 @@ class KNXExposeTime: def async_register(self): """Register listener.""" broadcast_type_string = self.type.upper() - broadcast_type = DateTimeBroadcastType[broadcast_type_string] + broadcast_type = broadcast_type_string self.device = DateTime( self.xknx, "Time", broadcast_type=broadcast_type, group_address=self.address ) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 29effaa7ebf..a18889e122a 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,60 +1,16 @@ """Support for KNX/IP binary sensors.""" -import voluptuous as vol -from xknx.devices import BinarySensor +from xknx.devices import BinarySensor as XknxBinarySensor -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from . import ATTR_DISCOVER_DEVICES, DATA_KNX, KNXAutomation - -CONF_STATE_ADDRESS = "state_address" -CONF_SIGNIFICANT_BIT = "significant_bit" -CONF_DEFAULT_SIGNIFICANT_BIT = 1 -CONF_SYNC_STATE = "sync_state" -CONF_AUTOMATION = "automation" -CONF_HOOK = "hook" -CONF_DEFAULT_HOOK = "on" -CONF_COUNTER = "counter" -CONF_DEFAULT_COUNTER = 1 -CONF_ACTION = "action" -CONF_RESET_AFTER = "reset_after" - -CONF__ACTION = "turn_off_action" - -DEFAULT_NAME = "KNX Binary Sensor" -AUTOMATION_SCHEMA = vol.Schema( - { - vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string, - vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port, - vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, - } -) - -AUTOMATIONS_SCHEMA = vol.All(cv.ensure_list, [AUTOMATION_SCHEMA]) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional( - CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT - ): cv.positive_int, - vol.Optional(CONF_SYNC_STATE, default=True): cv.boolean, - vol.Required(CONF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_DEVICE_CLASS): cv.string, - vol.Optional(CONF_RESET_AFTER): cv.positive_int, - vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA, - } -) +from . import ATTR_DISCOVER_DEVICES, DATA_KNX async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up binary sensor(s) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) - else: - async_add_entities_config(hass, config, async_add_entities) @callback @@ -67,48 +23,12 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): async_add_entities(entities) -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up binary senor for KNX platform configured within platform.""" - name = config[CONF_NAME] - - binary_sensor = BinarySensor( - hass.data[DATA_KNX].xknx, - name=name, - group_address_state=config[CONF_STATE_ADDRESS], - sync_state=config[CONF_SYNC_STATE], - device_class=config.get(CONF_DEVICE_CLASS), - significant_bit=config[CONF_SIGNIFICANT_BIT], - reset_after=config.get(CONF_RESET_AFTER), - ) - hass.data[DATA_KNX].xknx.devices.add(binary_sensor) - - entity = KNXBinarySensor(binary_sensor) - automations = config.get(CONF_AUTOMATION) - if automations is not None: - for automation in automations: - counter = automation[CONF_COUNTER] - hook = automation[CONF_HOOK] - action = automation[CONF_ACTION] - entity.automations.append( - KNXAutomation( - hass=hass, - device=binary_sensor, - hook=hook, - action=action, - counter=counter, - ) - ) - async_add_entities([entity]) - - class KNXBinarySensor(BinarySensorEntity): """Representation of a KNX binary sensor.""" - def __init__(self, device): + def __init__(self, device: XknxBinarySensor): """Initialize of KNX binary sensor.""" self.device = device - self.automations = [] @callback def async_register_callbacks(self): diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index a58e5312c11..db0559e5158 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,127 +1,31 @@ """Support for KNX/IP climate devices.""" from typing import List, Optional -import voluptuous as vol -from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode +from xknx.devices import Climate as XknxClimate from xknx.dpt import HVACOperationMode -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, - PRESET_COMFORT, - PRESET_ECO, - PRESET_SLEEP, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from . import ATTR_DISCOVER_DEVICES, DATA_KNX - -CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address" -CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address" -CONF_SETPOINT_SHIFT_STEP = "setpoint_shift_step" -CONF_SETPOINT_SHIFT_MAX = "setpoint_shift_max" -CONF_SETPOINT_SHIFT_MIN = "setpoint_shift_min" -CONF_TEMPERATURE_ADDRESS = "temperature_address" -CONF_TARGET_TEMPERATURE_ADDRESS = "target_temperature_address" -CONF_TARGET_TEMPERATURE_STATE_ADDRESS = "target_temperature_state_address" -CONF_OPERATION_MODE_ADDRESS = "operation_mode_address" -CONF_OPERATION_MODE_STATE_ADDRESS = "operation_mode_state_address" -CONF_CONTROLLER_STATUS_ADDRESS = "controller_status_address" -CONF_CONTROLLER_STATUS_STATE_ADDRESS = "controller_status_state_address" -CONF_CONTROLLER_MODE_ADDRESS = "controller_mode_address" -CONF_CONTROLLER_MODE_STATE_ADDRESS = "controller_mode_state_address" -CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = "operation_mode_frost_protection_address" -CONF_OPERATION_MODE_NIGHT_ADDRESS = "operation_mode_night_address" -CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address" -CONF_OPERATION_MODES = "operation_modes" -CONF_ON_OFF_ADDRESS = "on_off_address" -CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address" -CONF_ON_OFF_INVERT = "on_off_invert" -CONF_MIN_TEMP = "min_temp" -CONF_MAX_TEMP = "max_temp" - -DEFAULT_NAME = "KNX Climate" -DEFAULT_SETPOINT_SHIFT_STEP = 0.5 -DEFAULT_SETPOINT_SHIFT_MAX = 6 -DEFAULT_SETPOINT_SHIFT_MIN = -6 -DEFAULT_ON_OFF_INVERT = False -# Map KNX operation modes to HA modes. This list might not be full. -OPERATION_MODES = { - # Map DPT 201.105 HVAC control modes - "Auto": HVAC_MODE_AUTO, - "Heat": HVAC_MODE_HEAT, - "Cool": HVAC_MODE_COOL, - "Off": HVAC_MODE_OFF, - "Fan only": HVAC_MODE_FAN_ONLY, - "Dry": HVAC_MODE_DRY, -} +from .const import OPERATION_MODES, PRESET_MODES OPERATION_MODES_INV = dict(reversed(item) for item in OPERATION_MODES.items()) - -PRESET_MODES = { - # Map DPT 201.100 HVAC operating modes to HA presets - "Frost Protection": PRESET_ECO, - "Night": PRESET_SLEEP, - "Standby": PRESET_AWAY, - "Comfort": PRESET_COMFORT, -} - PRESET_MODES_INV = dict(reversed(item) for item in PRESET_MODES.items()) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional( - CONF_SETPOINT_SHIFT_STEP, default=DEFAULT_SETPOINT_SHIFT_STEP - ): vol.All(float, vol.Range(min=0, max=2)), - vol.Optional( - CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX - ): vol.All(int, vol.Range(min=0, max=32)), - vol.Optional( - CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN - ): vol.All(int, vol.Range(min=-32, max=0)), - vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string, - vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): cv.string, - vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string, - vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string, - vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string, - vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, - vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, - vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT): cv.boolean, - vol.Optional(CONF_OPERATION_MODES): vol.All( - cv.ensure_list, [vol.In({**OPERATION_MODES, **PRESET_MODES})] - ), - vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), - } -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up climate(s) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) - else: - async_add_entities_config(hass, config, async_add_entities) @callback @@ -134,68 +38,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): async_add_entities(entities) -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up climate for KNX platform configured within platform.""" - climate_mode = XknxClimateMode( - hass.data[DATA_KNX].xknx, - name=f"{config[CONF_NAME]} Mode", - group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS), - group_address_operation_mode_state=config.get( - CONF_OPERATION_MODE_STATE_ADDRESS - ), - group_address_controller_status=config.get(CONF_CONTROLLER_STATUS_ADDRESS), - group_address_controller_status_state=config.get( - CONF_CONTROLLER_STATUS_STATE_ADDRESS - ), - group_address_controller_mode=config.get(CONF_CONTROLLER_MODE_ADDRESS), - group_address_controller_mode_state=config.get( - CONF_CONTROLLER_MODE_STATE_ADDRESS - ), - group_address_operation_mode_protection=config.get( - CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS - ), - group_address_operation_mode_night=config.get( - CONF_OPERATION_MODE_NIGHT_ADDRESS - ), - group_address_operation_mode_comfort=config.get( - CONF_OPERATION_MODE_COMFORT_ADDRESS - ), - operation_modes=config.get(CONF_OPERATION_MODES), - ) - hass.data[DATA_KNX].xknx.devices.add(climate_mode) - - climate = XknxClimate( - hass.data[DATA_KNX].xknx, - name=config[CONF_NAME], - group_address_temperature=config[CONF_TEMPERATURE_ADDRESS], - group_address_target_temperature=config.get(CONF_TARGET_TEMPERATURE_ADDRESS), - group_address_target_temperature_state=config[ - CONF_TARGET_TEMPERATURE_STATE_ADDRESS - ], - group_address_setpoint_shift=config.get(CONF_SETPOINT_SHIFT_ADDRESS), - group_address_setpoint_shift_state=config.get( - CONF_SETPOINT_SHIFT_STATE_ADDRESS - ), - setpoint_shift_step=config[CONF_SETPOINT_SHIFT_STEP], - setpoint_shift_max=config[CONF_SETPOINT_SHIFT_MAX], - setpoint_shift_min=config[CONF_SETPOINT_SHIFT_MIN], - group_address_on_off=config.get(CONF_ON_OFF_ADDRESS), - group_address_on_off_state=config.get(CONF_ON_OFF_STATE_ADDRESS), - min_temp=config.get(CONF_MIN_TEMP), - max_temp=config.get(CONF_MAX_TEMP), - mode=climate_mode, - on_off_invert=config[CONF_ON_OFF_INVERT], - ) - hass.data[DATA_KNX].xknx.devices.add(climate) - - async_add_entities([KNXClimate(climate)]) - - class KNXClimate(ClimateEntity): """Representation of a KNX climate device.""" - def __init__(self, device): + def __init__(self, device: XknxClimate): """Initialize of a KNX climate device.""" self.device = device self._unit_of_measurement = TEMP_CELSIUS @@ -278,8 +124,6 @@ class KNXClimate(ClimateEntity): """Return current operation ie. heat, cool, idle.""" if self.device.supports_on_off and not self.device.is_on: return HVAC_MODE_OFF - if self.device.supports_on_off and self.device.is_on: - return HVAC_MODE_HEAT if self.device.mode.supports_operation_mode: return OPERATION_MODES.get( self.device.mode.operation_mode.value, HVAC_MODE_HEAT @@ -296,10 +140,11 @@ class KNXClimate(ClimateEntity): ] if self.device.supports_on_off: - _operations.append(HVAC_MODE_HEAT) + if not _operations: + _operations.append(HVAC_MODE_HEAT) _operations.append(HVAC_MODE_OFF) - _modes = list(filter(None, _operations)) + _modes = list(set(filter(None, _operations))) # default to ["heat"] return _modes if _modes else [HVAC_MODE_HEAT] @@ -307,12 +152,15 @@ class KNXClimate(ClimateEntity): """Set operation mode.""" if self.device.supports_on_off and hvac_mode == HVAC_MODE_OFF: await self.device.turn_off() - elif self.device.supports_on_off and hvac_mode == HVAC_MODE_HEAT: - await self.device.turn_on() - elif self.device.mode.supports_operation_mode: - knx_operation_mode = HVACOperationMode(OPERATION_MODES_INV.get(hvac_mode)) - await self.device.mode.set_operation_mode(knx_operation_mode) - self.async_write_ha_state() + else: + if self.device.supports_on_off and not self.device.is_on: + await self.device.turn_on() + if self.device.mode.supports_operation_mode: + knx_operation_mode = HVACOperationMode( + OPERATION_MODES_INV.get(hvac_mode) + ) + await self.device.mode.set_operation_mode(knx_operation_mode) + self.async_write_ha_state() @property def preset_mode(self) -> Optional[str]: diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py new file mode 100644 index 00000000000..fefb0cd73c0 --- /dev/null +++ b/homeassistant/components/knx/const.py @@ -0,0 +1,61 @@ +"""Constants for the KNX integration.""" +from enum import Enum + +from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_SLEEP, +) + +DOMAIN = "knx" +DATA_KNX = "data_knx" + +CONF_STATE_ADDRESS = "state_address" +CONF_SYNC_STATE = "sync_state" + + +class ColorTempModes(Enum): + """Color temperature modes for config validation.""" + + absolute = "DPT-7.600" + relative = "DPT-5.001" + + +class DeviceTypes(Enum): + """KNX device types.""" + + cover = "cover" + light = "light" + binary_sensor = "binary_sensor" + climate = "climate" + switch = "switch" + notify = "notify" + scene = "scene" + sensor = "sensor" + + +# Map KNX operation modes to HA modes. This list might not be complete. +OPERATION_MODES = { + # Map DPT 20.105 HVAC control modes + "Auto": HVAC_MODE_AUTO, + "Heat": HVAC_MODE_HEAT, + "Cool": HVAC_MODE_COOL, + "Off": HVAC_MODE_OFF, + "Fan only": HVAC_MODE_FAN_ONLY, + "Dry": HVAC_MODE_DRY, +} + +PRESET_MODES = { + # Map DPT 20.102 HVAC operating modes to HA presets + "Frost Protection": PRESET_ECO, + "Night": PRESET_SLEEP, + "Standby": PRESET_AWAY, + "Comfort": PRESET_COMFORT, +} diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 731105f6629..583f41c48ca 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -1,11 +1,10 @@ """Support for KNX/IP covers.""" -import voluptuous as vol from xknx.devices import Cover as XknxCover from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, - PLATFORM_SCHEMA, + DEVICE_CLASS_BLIND, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, @@ -13,53 +12,16 @@ from homeassistant.components.cover import ( SUPPORT_STOP, CoverEntity, ) -from homeassistant.const import CONF_NAME from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_utc_time_change from . import ATTR_DISCOVER_DEVICES, DATA_KNX -CONF_MOVE_LONG_ADDRESS = "move_long_address" -CONF_MOVE_SHORT_ADDRESS = "move_short_address" -CONF_POSITION_ADDRESS = "position_address" -CONF_POSITION_STATE_ADDRESS = "position_state_address" -CONF_ANGLE_ADDRESS = "angle_address" -CONF_ANGLE_STATE_ADDRESS = "angle_state_address" -CONF_TRAVELLING_TIME_DOWN = "travelling_time_down" -CONF_TRAVELLING_TIME_UP = "travelling_time_up" -CONF_INVERT_POSITION = "invert_position" -CONF_INVERT_ANGLE = "invert_angle" - -DEFAULT_TRAVEL_TIME = 25 -DEFAULT_NAME = "KNX Cover" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string, - vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string, - vol.Optional(CONF_POSITION_ADDRESS): cv.string, - vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string, - vol.Optional(CONF_ANGLE_ADDRESS): cv.string, - vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string, - vol.Optional( - CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME - ): cv.positive_int, - vol.Optional( - CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME - ): cv.positive_int, - vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, - vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, - } -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up cover(s) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) - else: - async_add_entities_config(hass, config, async_add_entities) @callback @@ -72,32 +34,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): async_add_entities(entities) -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up cover for KNX platform configured within platform.""" - cover = XknxCover( - hass.data[DATA_KNX].xknx, - name=config[CONF_NAME], - group_address_long=config.get(CONF_MOVE_LONG_ADDRESS), - group_address_short=config.get(CONF_MOVE_SHORT_ADDRESS), - group_address_position_state=config.get(CONF_POSITION_STATE_ADDRESS), - group_address_angle=config.get(CONF_ANGLE_ADDRESS), - group_address_angle_state=config.get(CONF_ANGLE_STATE_ADDRESS), - group_address_position=config.get(CONF_POSITION_ADDRESS), - travel_time_down=config[CONF_TRAVELLING_TIME_DOWN], - travel_time_up=config[CONF_TRAVELLING_TIME_UP], - invert_position=config[CONF_INVERT_POSITION], - invert_angle=config[CONF_INVERT_ANGLE], - ) - - hass.data[DATA_KNX].xknx.devices.add(cover) - async_add_entities([KNXCover(cover)]) - - class KNXCover(CoverEntity): """Representation of a KNX cover.""" - def __init__(self, device): + def __init__(self, device: XknxCover): """Initialize the cover.""" self.device = device self._unsubscribe_auto_updater = None @@ -109,6 +49,8 @@ class KNXCover(CoverEntity): async def after_update_callback(device): """Call after device was updated.""" self.async_write_ha_state() + if self.device.is_traveling(): + self.start_auto_updater() self.device.register_device_updated_cb(after_update_callback) @@ -135,44 +77,62 @@ class KNXCover(CoverEntity): """No polling needed within KNX.""" return False + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + if self.device.supports_angle: + return DEVICE_CLASS_BLIND + return None + @property def supported_features(self): """Flag supported features.""" - supported_features = ( - SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_STOP - ) + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + if self.device.supports_stop: + supported_features |= SUPPORT_STOP if self.device.supports_angle: supported_features |= SUPPORT_SET_TILT_POSITION return supported_features @property def current_cover_position(self): - """Return the current position of the cover.""" - return self.device.current_position() + """Return the current position of the cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + # In KNX 0 is open, 100 is closed. + try: + return 100 - self.device.current_position() + except TypeError: + return None @property def is_closed(self): """Return if the cover is closed.""" return self.device.is_closed() + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self.device.is_opening() + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self.device.is_closing() + async def async_close_cover(self, **kwargs): """Close the cover.""" - if not self.device.is_closed(): - await self.device.set_down() - self.start_auto_updater() + await self.device.set_down() async def async_open_cover(self, **kwargs): """Open the cover.""" - if not self.device.is_open(): - await self.device.set_up() - self.start_auto_updater() + await self.device.set_up() async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - if ATTR_POSITION in kwargs: - position = kwargs[ATTR_POSITION] - await self.device.set_position(position) - self.start_auto_updater() + knx_position = 100 - kwargs[ATTR_POSITION] + await self.device.set_position(knx_position) async def async_stop_cover(self, **kwargs): """Stop the cover.""" @@ -184,13 +144,15 @@ class KNXCover(CoverEntity): """Return current tilt position of cover.""" if not self.device.supports_angle: return None - return self.device.current_angle() + try: + return 100 - self.device.current_angle() + except TypeError: + return None async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - if ATTR_TILT_POSITION in kwargs: - tilt_position = kwargs[ATTR_TILT_POSITION] - await self.device.set_angle(tilt_position) + knx_tilt_position = 100 - kwargs[ATTR_TILT_POSITION] + await self.device.set_angle(knx_tilt_position) def start_auto_updater(self): """Start the autoupdater to update Home Assistant while cover is moving.""" diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py new file mode 100644 index 00000000000..f53a7436122 --- /dev/null +++ b/homeassistant/components/knx/factory.py @@ -0,0 +1,263 @@ +"""Factory function to initialize KNX devices from config.""" +from xknx import XKNX +from xknx.devices import ( + ActionCallback as XknxActionCallback, + BinarySensor as XknxBinarySensor, + Climate as XknxClimate, + ClimateMode as XknxClimateMode, + Cover as XknxCover, + Device as XknxDevice, + Light as XknxLight, + Notification as XknxNotification, + Scene as XknxScene, + Sensor as XknxSensor, + Switch as XknxSwitch, +) + +from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.script import Script +from homeassistant.helpers.typing import ConfigType + +from .const import DATA_KNX, DOMAIN, ColorTempModes, DeviceTypes +from .schema import ( + BinarySensorSchema, + ClimateSchema, + CoverSchema, + LightSchema, + SceneSchema, + SensorSchema, + SwitchSchema, +) + + +def create_knx_device( + hass: HomeAssistant, device_type: DeviceTypes, knx_module: XKNX, config: ConfigType +) -> XknxDevice: + """Return the requested XKNX device.""" + if device_type is DeviceTypes.light: + return _create_light(knx_module, config) + + if device_type is DeviceTypes.cover: + return _create_cover(knx_module, config) + + if device_type is DeviceTypes.climate: + return _create_climate(hass, knx_module, config) + + if device_type is DeviceTypes.switch: + return _create_switch(knx_module, config) + + if device_type is DeviceTypes.sensor: + return _create_sensor(knx_module, config) + + if device_type is DeviceTypes.notify: + return _create_notify(knx_module, config) + + if device_type is DeviceTypes.scene: + return _create_scene(knx_module, config) + + if device_type is DeviceTypes.binary_sensor: + return _create_binary_sensor(hass, knx_module, config) + + +def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: + """Return a KNX Cover device to be used within XKNX.""" + return XknxCover( + knx_module, + name=config[CONF_NAME], + group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS), + group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS), + group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS), + group_address_position_state=config.get( + CoverSchema.CONF_POSITION_STATE_ADDRESS + ), + group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS), + group_address_angle_state=config.get(CoverSchema.CONF_ANGLE_STATE_ADDRESS), + group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS), + travel_time_down=config[CoverSchema.CONF_TRAVELLING_TIME_DOWN], + travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP], + invert_position=config[CoverSchema.CONF_INVERT_POSITION], + invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], + ) + + +def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: + """Return a KNX Light device to be used within XKNX.""" + group_address_tunable_white = None + group_address_tunable_white_state = None + group_address_color_temp = None + group_address_color_temp_state = None + if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.absolute: + group_address_color_temp = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS) + group_address_color_temp_state = config.get( + LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS + ) + elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.relative: + group_address_tunable_white = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS) + group_address_tunable_white_state = config.get( + LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS + ) + + return XknxLight( + knx_module, + name=config[CONF_NAME], + group_address_switch=config[CONF_ADDRESS], + group_address_switch_state=config.get(LightSchema.CONF_STATE_ADDRESS), + group_address_brightness=config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS), + group_address_brightness_state=config.get( + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + ), + group_address_color=config.get(LightSchema.CONF_COLOR_ADDRESS), + group_address_color_state=config.get(LightSchema.CONF_COLOR_STATE_ADDRESS), + group_address_rgbw=config.get(LightSchema.CONF_RGBW_ADDRESS), + group_address_rgbw_state=config.get(LightSchema.CONF_RGBW_STATE_ADDRESS), + group_address_tunable_white=group_address_tunable_white, + group_address_tunable_white_state=group_address_tunable_white_state, + group_address_color_temperature=group_address_color_temp, + group_address_color_temperature_state=group_address_color_temp_state, + min_kelvin=config[LightSchema.CONF_MIN_KELVIN], + max_kelvin=config[LightSchema.CONF_MAX_KELVIN], + ) + + +def _create_climate( + hass: HomeAssistant, knx_module: XKNX, config: ConfigType +) -> XknxClimate: + """Return a KNX Climate device to be used within XKNX.""" + climate_mode = XknxClimateMode( + knx_module, + name=f"{config[CONF_NAME]} Mode", + group_address_operation_mode=config.get( + ClimateSchema.CONF_OPERATION_MODE_ADDRESS + ), + group_address_operation_mode_state=config.get( + ClimateSchema.CONF_OPERATION_MODE_STATE_ADDRESS + ), + group_address_controller_status=config.get( + ClimateSchema.CONF_CONTROLLER_STATUS_ADDRESS + ), + group_address_controller_status_state=config.get( + ClimateSchema.CONF_CONTROLLER_STATUS_STATE_ADDRESS + ), + group_address_controller_mode=config.get( + ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS + ), + group_address_controller_mode_state=config.get( + ClimateSchema.CONF_CONTROLLER_MODE_STATE_ADDRESS + ), + group_address_operation_mode_protection=config.get( + ClimateSchema.CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS + ), + group_address_operation_mode_night=config.get( + ClimateSchema.CONF_OPERATION_MODE_NIGHT_ADDRESS + ), + group_address_operation_mode_comfort=config.get( + ClimateSchema.CONF_OPERATION_MODE_COMFORT_ADDRESS + ), + group_address_operation_mode_standby=config.get( + ClimateSchema.CONF_OPERATION_MODE_STANDBY_ADDRESS + ), + group_address_heat_cool=config.get(ClimateSchema.CONF_HEAT_COOL_ADDRESS), + group_address_heat_cool_state=config.get( + ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS + ), + operation_modes=config.get(ClimateSchema.CONF_OPERATION_MODES), + ) + hass.data[DATA_KNX].xknx.devices.add(climate_mode) + + return XknxClimate( + knx_module, + name=config[CONF_NAME], + group_address_temperature=config[ClimateSchema.CONF_TEMPERATURE_ADDRESS], + group_address_target_temperature=config.get( + ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS + ), + group_address_target_temperature_state=config[ + ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS + ], + group_address_setpoint_shift=config.get( + ClimateSchema.CONF_SETPOINT_SHIFT_ADDRESS + ), + group_address_setpoint_shift_state=config.get( + ClimateSchema.CONF_SETPOINT_SHIFT_STATE_ADDRESS + ), + setpoint_shift_mode=config[ClimateSchema.CONF_SETPOINT_SHIFT_MODE], + setpoint_shift_max=config[ClimateSchema.CONF_SETPOINT_SHIFT_MAX], + setpoint_shift_min=config[ClimateSchema.CONF_SETPOINT_SHIFT_MIN], + temperature_step=config[ClimateSchema.CONF_TEMPERATURE_STEP], + group_address_on_off=config.get(ClimateSchema.CONF_ON_OFF_ADDRESS), + group_address_on_off_state=config.get(ClimateSchema.CONF_ON_OFF_STATE_ADDRESS), + min_temp=config.get(ClimateSchema.CONF_MIN_TEMP), + max_temp=config.get(ClimateSchema.CONF_MAX_TEMP), + mode=climate_mode, + on_off_invert=config[ClimateSchema.CONF_ON_OFF_INVERT], + ) + + +def _create_switch(knx_module: XKNX, config: ConfigType) -> XknxSwitch: + """Return a KNX switch to be used within XKNX.""" + return XknxSwitch( + knx_module, + name=config[CONF_NAME], + group_address=config[CONF_ADDRESS], + group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS), + ) + + +def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: + """Return a KNX sensor to be used within XKNX.""" + return XknxSensor( + knx_module, + name=config[CONF_NAME], + group_address_state=config[SensorSchema.CONF_STATE_ADDRESS], + sync_state=config[SensorSchema.CONF_SYNC_STATE], + value_type=config[CONF_TYPE], + ) + + +def _create_notify(knx_module: XKNX, config: ConfigType) -> XknxNotification: + """Return a KNX notification to be used within XKNX.""" + return XknxNotification( + knx_module, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], + ) + + +def _create_scene(knx_module: XKNX, config: ConfigType) -> XknxScene: + """Return a KNX scene to be used within XKNX.""" + return XknxScene( + knx_module, + name=config[CONF_NAME], + group_address=config[CONF_ADDRESS], + scene_number=config[SceneSchema.CONF_SCENE_NUMBER], + ) + + +def _create_binary_sensor( + hass: HomeAssistant, knx_module: XKNX, config: ConfigType +) -> XknxBinarySensor: + """Return a KNX binary sensor to be used within XKNX.""" + device_name = config[CONF_NAME] + actions = [] + automations = config.get(BinarySensorSchema.CONF_AUTOMATION) + if automations is not None: + for automation in automations: + counter = automation[BinarySensorSchema.CONF_COUNTER] + hook = automation[BinarySensorSchema.CONF_HOOK] + action = automation[BinarySensorSchema.CONF_ACTION] + script_name = f"{device_name} turn ON script" + script = Script(hass, action, script_name, DOMAIN) + action = XknxActionCallback( + knx_module, script.async_run, hook=hook, counter=counter + ) + actions.append(action) + + return XknxBinarySensor( + knx_module, + name=device_name, + group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS], + sync_state=config[BinarySensorSchema.CONF_SYNC_STATE], + device_class=config.get(CONF_DEVICE_CLASS), + ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE], + reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), + actions=actions, + ) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 7ea5dc52155..ac0bf2122a8 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -1,7 +1,4 @@ """Support for KNX/IP lights.""" -from enum import Enum - -import voluptuous as vol from xknx.devices import Light as XknxLight from homeassistant.components.light import ( @@ -9,81 +6,26 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_WHITE_VALUE, - PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util from . import ATTR_DISCOVER_DEVICES, DATA_KNX -CONF_STATE_ADDRESS = "state_address" -CONF_BRIGHTNESS_ADDRESS = "brightness_address" -CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address" -CONF_COLOR_ADDRESS = "color_address" -CONF_COLOR_STATE_ADDRESS = "color_state_address" -CONF_COLOR_TEMP_ADDRESS = "color_temperature_address" -CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address" -CONF_COLOR_TEMP_MODE = "color_temperature_mode" -CONF_RGBW_ADDRESS = "rgbw_address" -CONF_RGBW_STATE_ADDRESS = "rgbw_state_address" -CONF_MIN_KELVIN = "min_kelvin" -CONF_MAX_KELVIN = "max_kelvin" - -DEFAULT_NAME = "KNX Light" DEFAULT_COLOR = (0.0, 0.0) DEFAULT_BRIGHTNESS = 255 -DEFAULT_COLOR_TEMP_MODE = "absolute" DEFAULT_WHITE_VALUE = 255 -DEFAULT_MIN_KELVIN = 2700 # 370 mireds -DEFAULT_MAX_KELVIN = 6000 # 166 mireds - - -class ColorTempModes(Enum): - """Color temperature modes for config validation.""" - - absolute = "DPT-7.600" - relative = "DPT-5.001" - - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string, - vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE): cv.enum( - ColorTempModes - ), - vol.Optional(CONF_RGBW_ADDRESS): cv.string, - vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string, - vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - } -) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up lights for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) - else: - async_add_entities_config(hass, config, async_add_entities) @callback @@ -96,46 +38,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): async_add_entities(entities) -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up light for KNX platform configured within platform.""" - group_address_tunable_white = None - group_address_tunable_white_state = None - group_address_color_temp = None - group_address_color_temp_state = None - if config[CONF_COLOR_TEMP_MODE] == ColorTempModes.absolute: - group_address_color_temp = config.get(CONF_COLOR_TEMP_ADDRESS) - group_address_color_temp_state = config.get(CONF_COLOR_TEMP_STATE_ADDRESS) - elif config[CONF_COLOR_TEMP_MODE] == ColorTempModes.relative: - group_address_tunable_white = config.get(CONF_COLOR_TEMP_ADDRESS) - group_address_tunable_white_state = config.get(CONF_COLOR_TEMP_STATE_ADDRESS) - - light = XknxLight( - hass.data[DATA_KNX].xknx, - name=config[CONF_NAME], - group_address_switch=config[CONF_ADDRESS], - group_address_switch_state=config.get(CONF_STATE_ADDRESS), - group_address_brightness=config.get(CONF_BRIGHTNESS_ADDRESS), - group_address_brightness_state=config.get(CONF_BRIGHTNESS_STATE_ADDRESS), - group_address_color=config.get(CONF_COLOR_ADDRESS), - group_address_color_state=config.get(CONF_COLOR_STATE_ADDRESS), - group_address_rgbw=config.get(CONF_RGBW_ADDRESS), - group_address_rgbw_state=config.get(CONF_RGBW_STATE_ADDRESS), - group_address_tunable_white=group_address_tunable_white, - group_address_tunable_white_state=group_address_tunable_white_state, - group_address_color_temperature=group_address_color_temp, - group_address_color_temperature_state=group_address_color_temp_state, - min_kelvin=config[CONF_MIN_KELVIN], - max_kelvin=config[CONF_MAX_KELVIN], - ) - hass.data[DATA_KNX].xknx.devices.add(light) - async_add_entities([KNXLight(light)]) - - class KNXLight(LightEntity): """Representation of a KNX light.""" - def __init__(self, device): + def __init__(self, device: XknxLight): """Initialize of KNX light.""" self.device = device diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 941a62d2d14..108f3a2062a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,6 +2,6 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.11.3"], - "codeowners": ["@Julius2342"] + "requirements": ["xknx==0.12.0"], + "codeowners": ["@Julius2342", "@farmio", "@marvin-w"] } diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 64d513b8624..fcb5bd352d5 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,31 +1,16 @@ """Support for KNX/IP notification services.""" -import voluptuous as vol from xknx.devices import Notification as XknxNotification -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import CONF_ADDRESS, CONF_NAME +from homeassistant.components.notify import BaseNotificationService from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from . import ATTR_DISCOVER_DEVICES, DATA_KNX -DEFAULT_NAME = "KNX Notify" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - async def async_get_service(hass, config, discovery_info=None): """Get the KNX notification service.""" - return ( + if discovery_info is not None: async_get_service_discovery(hass, discovery_info) - if discovery_info is not None - else async_get_service_config(hass, config) - ) @callback @@ -40,22 +25,10 @@ def async_get_service_discovery(hass, discovery_info): ) -@callback -def async_get_service_config(hass, config): - """Set up notification for KNX platform configured within platform.""" - notification = XknxNotification( - hass.data[DATA_KNX].xknx, - name=config[CONF_NAME], - group_address=config[CONF_ADDRESS], - ) - hass.data[DATA_KNX].xknx.devices.add(notification) - return KNXNotificationService([notification]) - - class KNXNotificationService(BaseNotificationService): """Implement demo notification service.""" - def __init__(self, devices): + def __init__(self, devices: XknxNotification): """Initialize the service.""" self.devices = devices diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 8f2c24c05b6..dfa667dcd4f 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -1,35 +1,18 @@ """Support for KNX scenes.""" from typing import Any -import voluptuous as vol from xknx.devices import Scene as XknxScene -from homeassistant.components.scene import CONF_PLATFORM, Scene -from homeassistant.const import CONF_ADDRESS, CONF_NAME +from homeassistant.components.scene import Scene from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from . import ATTR_DISCOVER_DEVICES, DATA_KNX -CONF_SCENE_NUMBER = "scene_number" - -DEFAULT_NAME = "KNX SCENE" -PLATFORM_SCHEMA = vol.Schema( - { - vol.Required(CONF_PLATFORM): "knx", - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ADDRESS): cv.string, - vol.Required(CONF_SCENE_NUMBER): cv.positive_int, - } -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the scenes for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) - else: - async_add_entities_config(hass, config, async_add_entities) @callback @@ -42,23 +25,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): async_add_entities(entities) -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up scene for KNX platform configured within platform.""" - scene = XknxScene( - hass.data[DATA_KNX].xknx, - name=config[CONF_NAME], - group_address=config[CONF_ADDRESS], - scene_number=config[CONF_SCENE_NUMBER], - ) - hass.data[DATA_KNX].xknx.devices.add(scene) - async_add_entities([KNXScene(scene)]) - - class KNXScene(Scene): """Representation of a KNX scene.""" - def __init__(self, scene): + def __init__(self, scene: XknxScene): """Init KNX scene.""" self.scene = scene diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py new file mode 100644 index 00000000000..3b9436a4eee --- /dev/null +++ b/homeassistant/components/knx/schema.py @@ -0,0 +1,342 @@ +"""Voluptuous schemas for the KNX integration.""" +import voluptuous as vol +from xknx.devices.climate import SetpointShiftMode + +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE_CLASS, + CONF_ENTITY_ID, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_STATE_ADDRESS, + CONF_SYNC_STATE, + OPERATION_MODES, + PRESET_MODES, + ColorTempModes, +) + + +class ConnectionSchema: + """Voluptuous schema for KNX connection.""" + + CONF_KNX_LOCAL_IP = "local_ip" + + TUNNELING_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_KNX_LOCAL_IP): cv.string, + vol.Optional(CONF_PORT): cv.port, + } + ) + + ROUTING_SCHEMA = vol.Schema({vol.Optional(CONF_KNX_LOCAL_IP): cv.string}) + + +class CoverSchema: + """Voluptuous schema for KNX covers.""" + + CONF_MOVE_LONG_ADDRESS = "move_long_address" + CONF_MOVE_SHORT_ADDRESS = "move_short_address" + CONF_STOP_ADDRESS = "stop_address" + CONF_POSITION_ADDRESS = "position_address" + CONF_POSITION_STATE_ADDRESS = "position_state_address" + CONF_ANGLE_ADDRESS = "angle_address" + CONF_ANGLE_STATE_ADDRESS = "angle_state_address" + CONF_TRAVELLING_TIME_DOWN = "travelling_time_down" + CONF_TRAVELLING_TIME_UP = "travelling_time_up" + CONF_INVERT_POSITION = "invert_position" + CONF_INVERT_ANGLE = "invert_angle" + + DEFAULT_TRAVEL_TIME = 25 + DEFAULT_NAME = "KNX Cover" + + SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string, + vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string, + vol.Optional(CONF_STOP_ADDRESS): cv.string, + vol.Optional(CONF_POSITION_ADDRESS): cv.string, + vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string, + vol.Optional(CONF_ANGLE_ADDRESS): cv.string, + vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string, + vol.Optional( + CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME + ): cv.positive_int, + vol.Optional( + CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME + ): cv.positive_int, + vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, + vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, + } + ) + + +class BinarySensorSchema: + """Voluptuous schema for KNX binary sensors.""" + + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS + CONF_SYNC_STATE = CONF_SYNC_STATE + CONF_IGNORE_INTERNAL_STATE = "ignore_internal_state" + CONF_AUTOMATION = "automation" + CONF_HOOK = "hook" + CONF_DEFAULT_HOOK = "on" + CONF_COUNTER = "counter" + CONF_DEFAULT_COUNTER = 1 + CONF_ACTION = "action" + CONF_RESET_AFTER = "reset_after" + + DEFAULT_NAME = "KNX Binary Sensor" + AUTOMATION_SCHEMA = vol.Schema( + { + vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string, + vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port, + vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, + } + ) + + AUTOMATIONS_SCHEMA = vol.All(cv.ensure_list, [AUTOMATION_SCHEMA]) + + SCHEMA = vol.All( + cv.deprecated("significant_bit"), + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SYNC_STATE, default=True): vol.Any( + vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), + cv.boolean, + cv.string, + ), + vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean, + vol.Required(CONF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_DEVICE_CLASS): cv.string, + vol.Optional(CONF_RESET_AFTER): cv.positive_int, + vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA, + } + ), + ) + + +class LightSchema: + """Voluptuous schema for KNX lights.""" + + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS + CONF_BRIGHTNESS_ADDRESS = "brightness_address" + CONF_BRIGHTNESS_STATE_ADDRESS = "brightness_state_address" + CONF_COLOR_ADDRESS = "color_address" + CONF_COLOR_STATE_ADDRESS = "color_state_address" + CONF_COLOR_TEMP_ADDRESS = "color_temperature_address" + CONF_COLOR_TEMP_STATE_ADDRESS = "color_temperature_state_address" + CONF_COLOR_TEMP_MODE = "color_temperature_mode" + CONF_RGBW_ADDRESS = "rgbw_address" + CONF_RGBW_STATE_ADDRESS = "rgbw_state_address" + CONF_MIN_KELVIN = "min_kelvin" + CONF_MAX_KELVIN = "max_kelvin" + + DEFAULT_NAME = "KNX Light" + DEFAULT_COLOR_TEMP_MODE = "absolute" + DEFAULT_MIN_KELVIN = 2700 # 370 mireds + DEFAULT_MAX_KELVIN = 6000 # 166 mireds + + SCHEMA = vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string, + vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string, + vol.Optional( + CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE + ): cv.enum(ColorTempModes), + vol.Optional(CONF_RGBW_ADDRESS): cv.string, + vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string, + vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + } + ) + + +class ClimateSchema: + """Voluptuous schema for KNX climate devices.""" + + CONF_SETPOINT_SHIFT_ADDRESS = "setpoint_shift_address" + CONF_SETPOINT_SHIFT_STATE_ADDRESS = "setpoint_shift_state_address" + CONF_SETPOINT_SHIFT_MODE = "setpoint_shift_mode" + CONF_SETPOINT_SHIFT_MAX = "setpoint_shift_max" + CONF_SETPOINT_SHIFT_MIN = "setpoint_shift_min" + CONF_TEMPERATURE_ADDRESS = "temperature_address" + CONF_TEMPERATURE_STEP = "temperature_step" + CONF_TARGET_TEMPERATURE_ADDRESS = "target_temperature_address" + CONF_TARGET_TEMPERATURE_STATE_ADDRESS = "target_temperature_state_address" + CONF_OPERATION_MODE_ADDRESS = "operation_mode_address" + CONF_OPERATION_MODE_STATE_ADDRESS = "operation_mode_state_address" + CONF_CONTROLLER_STATUS_ADDRESS = "controller_status_address" + CONF_CONTROLLER_STATUS_STATE_ADDRESS = "controller_status_state_address" + CONF_CONTROLLER_MODE_ADDRESS = "controller_mode_address" + CONF_CONTROLLER_MODE_STATE_ADDRESS = "controller_mode_state_address" + CONF_HEAT_COOL_ADDRESS = "heat_cool_address" + CONF_HEAT_COOL_STATE_ADDRESS = "heat_cool_state_address" + CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = ( + "operation_mode_frost_protection_address" + ) + CONF_OPERATION_MODE_NIGHT_ADDRESS = "operation_mode_night_address" + CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address" + CONF_OPERATION_MODE_STANDBY_ADDRESS = "operation_mode_standby_address" + CONF_OPERATION_MODES = "operation_modes" + CONF_ON_OFF_ADDRESS = "on_off_address" + CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address" + CONF_ON_OFF_INVERT = "on_off_invert" + CONF_MIN_TEMP = "min_temp" + CONF_MAX_TEMP = "max_temp" + + DEFAULT_NAME = "KNX Climate" + DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010" + DEFAULT_SETPOINT_SHIFT_MAX = 6 + DEFAULT_SETPOINT_SHIFT_MIN = -6 + DEFAULT_TEMPERATURE_STEP = 0.1 + DEFAULT_ON_OFF_INVERT = False + + SCHEMA = vol.All( + cv.deprecated("setpoint_shift_step", replacement_key=CONF_TEMPERATURE_STEP), + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_SETPOINT_SHIFT_MODE, default=DEFAULT_SETPOINT_SHIFT_MODE + ): cv.enum(SetpointShiftMode), + vol.Optional( + CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX + ): vol.All(int, vol.Range(min=0, max=32)), + vol.Optional( + CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN + ): vol.All(int, vol.Range(min=-32, max=0)), + vol.Optional( + CONF_TEMPERATURE_STEP, default=DEFAULT_TEMPERATURE_STEP + ): vol.All(float, vol.Range(min=0, max=2)), + vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string, + vol.Required(CONF_TARGET_TEMPERATURE_STATE_ADDRESS): cv.string, + vol.Optional(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string, + vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string, + vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string, + vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string, + vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string, + vol.Optional(CONF_CONTROLLER_MODE_ADDRESS): cv.string, + vol.Optional(CONF_CONTROLLER_MODE_STATE_ADDRESS): cv.string, + vol.Optional(CONF_HEAT_COOL_ADDRESS): cv.string, + vol.Optional(CONF_HEAT_COOL_STATE_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, + vol.Optional(CONF_OPERATION_MODE_STANDBY_ADDRESS): cv.string, + vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, + vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, + vol.Optional( + CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT + ): cv.boolean, + vol.Optional(CONF_OPERATION_MODES): vol.All( + cv.ensure_list, [vol.In({**OPERATION_MODES, **PRESET_MODES})] + ), + vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), + } + ), + ) + + +class SwitchSchema: + """Voluptuous schema for KNX switches.""" + + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS + + DEFAULT_NAME = "KNX Switch" + SCHEMA = vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_ADDRESS): cv.string, + } + ) + + +class ExposeSchema: + """Voluptuous schema for KNX exposures.""" + + CONF_KNX_EXPOSE_TYPE = CONF_TYPE + CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" + CONF_KNX_EXPOSE_DEFAULT = "default" + CONF_KNX_EXPOSE_ADDRESS = CONF_ADDRESS + + SCHEMA = vol.Schema( + { + vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any(int, float, str), + vol.Optional(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(CONF_KNX_EXPOSE_ATTRIBUTE): cv.string, + vol.Optional(CONF_KNX_EXPOSE_DEFAULT): cv.match_all, + vol.Required(CONF_KNX_EXPOSE_ADDRESS): cv.string, + } + ) + + +class NotifySchema: + """Voluptuous schema for KNX notifications.""" + + DEFAULT_NAME = "KNX Notify" + + SCHEMA = vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } + ) + + +class SensorSchema: + """Voluptuous schema for KNX sensors.""" + + CONF_STATE_ADDRESS = CONF_STATE_ADDRESS + CONF_SYNC_STATE = CONF_SYNC_STATE + DEFAULT_NAME = "KNX Sensor" + + SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SYNC_STATE, default=True): vol.Any( + vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), + cv.boolean, + cv.string, + ), + vol.Required(CONF_STATE_ADDRESS): cv.string, + vol.Required(CONF_TYPE): vol.Any(int, float, str), + } + ) + + +class SceneSchema: + """Voluptuous schema for KNX scenes.""" + + CONF_SCENE_NUMBER = "scene_number" + + DEFAULT_NAME = "KNX SCENE" + SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_SCENE_NUMBER): cv.positive_int, + } + ) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 2d278ec04b4..1fd8950a3fb 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,35 +1,16 @@ """Support for KNX/IP sensors.""" -import voluptuous as vol from xknx.devices import Sensor as XknxSensor -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from . import ATTR_DISCOVER_DEVICES, DATA_KNX -CONF_STATE_ADDRESS = "state_address" -CONF_SYNC_STATE = "sync_state" -DEFAULT_NAME = "KNX Sensor" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SYNC_STATE, default=True): cv.boolean, - vol.Required(CONF_STATE_ADDRESS): cv.string, - vol.Required(CONF_TYPE): cv.string, - } -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up sensor(s) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) - else: - async_add_entities_config(hass, config, async_add_entities) @callback @@ -42,24 +23,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): async_add_entities(entities) -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up sensor for KNX platform configured within platform.""" - sensor = XknxSensor( - hass.data[DATA_KNX].xknx, - name=config[CONF_NAME], - group_address_state=config[CONF_STATE_ADDRESS], - sync_state=config[CONF_SYNC_STATE], - value_type=config[CONF_TYPE], - ) - hass.data[DATA_KNX].xknx.devices.add(sensor) - async_add_entities([KNXSensor(sensor)]) - - class KNXSensor(Entity): """Representation of a KNX sensor.""" - def __init__(self, device): + def __init__(self, device: XknxSensor): """Initialize of a KNX sensor.""" self.device = device diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 5faaf0678d1..03d4e69b32c 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -7,3 +7,6 @@ send: payload: description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length." example: "[0, 4]" + type: + description: "Optional. If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." + example: "temperature" diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 00b98f0224b..a6e7e583b88 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -1,32 +1,16 @@ """Support for KNX/IP switches.""" -import voluptuous as vol from xknx.devices import Switch as XknxSwitch -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_ADDRESS, CONF_NAME +from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from . import ATTR_DISCOVER_DEVICES, DATA_KNX -CONF_STATE_ADDRESS = "state_address" - -DEFAULT_NAME = "KNX Switch" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_ADDRESS): cv.string, - } -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up switch(es) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) - else: - async_add_entities_config(hass, config, async_add_entities) @callback @@ -39,23 +23,10 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): async_add_entities(entities) -@callback -def async_add_entities_config(hass, config, async_add_entities): - """Set up switch for KNX platform configured within platform.""" - switch = XknxSwitch( - hass.data[DATA_KNX].xknx, - name=config[CONF_NAME], - group_address=config[CONF_ADDRESS], - group_address_state=config.get(CONF_STATE_ADDRESS), - ) - hass.data[DATA_KNX].xknx.devices.add(switch) - async_add_entities([KNXSwitch(switch)]) - - class KNXSwitch(SwitchEntity): """Representation of a KNX switch.""" - def __init__(self, device): + def __init__(self, device: XknxSwitch): """Initialize of KNX switch.""" self.device = device diff --git a/requirements_all.txt b/requirements_all.txt index c26e0322b17..39e6f82e6d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2252,7 +2252,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.11.3 +xknx==0.12.0 # homeassistant.components.bluesound # homeassistant.components.rest From 1bc4de2bd389ecc6841b80373025d9543898e8d4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 26 Aug 2020 16:24:44 -0500 Subject: [PATCH 364/862] Add tests for Plex media browser (#39220) --- tests/components/plex/mock_classes.py | 184 ++++++++++++++++++--- tests/components/plex/test_browse_media.py | 165 ++++++++++++++++++ tests/components/plex/test_server.py | 62 +++---- 3 files changed, 354 insertions(+), 57 deletions(-) create mode 100644 tests/components/plex/test_browse_media.py diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index dd8e9a93ab8..f05c1017023 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,4 +1,9 @@ """Mock classes used in tests.""" +from functools import lru_cache + +from aiohttp.helpers import reify +from plexapi.exceptions import NotFound + from homeassistant.components.plex.const import ( CONF_SERVER, CONF_SERVER_IDENTIFIER, @@ -116,11 +121,15 @@ class MockPlexServer: self._systemAccounts = list(map(MockPlexSystemAccount, range(num_users))) + self._library = None + self._clients = [] self._sessions = [] self.set_clients(num_users) self.set_sessions(num_users, session_type) + self._cache = {} + def set_clients(self, num_clients): """Set up mock PlexClients for this PlexServer.""" self._clients = [MockPlexClient(self._baseurl, x) for x in range(num_clients)] @@ -166,18 +175,32 @@ class MockPlexServer: """Mock version of PlexServer.""" return "1.0" - @property + @reify def library(self): """Mock library object of PlexServer.""" - return MockPlexLibrary() + return MockPlexLibrary(self) def playlist(self, playlist): """Mock the playlist lookup method.""" return MockPlexMediaItem(playlist, mediatype="playlist") + @lru_cache() + def playlists(self): + """Mock the playlists lookup method with a lazy init.""" + return [ + MockPlexPlaylist( + self.library.section("Movies").all() + + self.library.section("TV Shows").all() + ), + MockPlexPlaylist(self.library.section("Music").all()), + ] + def fetchItem(self, item): """Mock the fetchItem method.""" - return MockPlexMediaItem("Item Name") + for section in self.library.sections(): + result = section.fetchItem(item) + if result: + return result class MockPlexClient: @@ -247,7 +270,7 @@ class MockPlexSession: self.TYPE = mediatype self.usernames = [list(MOCK_USERS)[index]] self.players = [player] - self._section = MockPlexLibrarySection() + self._section = MockPlexLibrarySection("Movies") @property def duration(self): @@ -302,26 +325,80 @@ class MockPlexSession: class MockPlexLibrary: """Mock a Plex Library instance.""" - def __init__(self): + def __init__(self, plex_server): """Initialize the object.""" + self._plex_server = plex_server + self._sections = {} - def section(self, library_name): + for kind in ["Movies", "Music", "TV Shows", "Photos"]: + self._sections[kind] = MockPlexLibrarySection(kind) + + def section(self, title): """Mock the LibrarySection lookup.""" - return MockPlexLibrarySection(library_name) + section = self._sections.get(title) + if section: + return section + raise NotFound + + def sections(self): + """Return all available sections.""" + return self._sections.values() + + def sectionByID(self, section_id): + """Mock the sectionByID lookup.""" + return [x for x in self.sections() if x.key == section_id][0] class MockPlexLibrarySection: """Mock a Plex LibrarySection instance.""" - def __init__(self, library="Movies"): + def __init__(self, library): """Initialize the object.""" self.title = library + if library == "Music": + self._item = MockPlexArtist("Artist") + elif library == "TV Shows": + self._item = MockPlexShow("TV Show") + else: + self._item = MockPlexMediaItem(library[:-1]) + def get(self, query): """Mock the get lookup method.""" + if self._item.title == query: + return self._item + raise NotFound + + def all(self): + """Mock the all method.""" + return [self._item] + + def fetchItem(self, ratingKey): + """Return a specific item.""" + for item in self.all(): + if item.ratingKey == ratingKey: + return item + if item._children: + for child in item._children: + if child.ratingKey == ratingKey: + return child + + @property + def type(self): + """Mock the library type.""" + if self.title == "Movies": + return "movie" if self.title == "Music": - return MockPlexArtist(query) - return MockPlexMediaItem(query) + return "artist" + if self.title == "TV Shows": + return "show" + if self.title == "Photos": + return "photo" + + @property + def key(self): + """Mock the key identifier property.""" + return str(id(self.title)) class MockPlexMediaItem: @@ -331,27 +408,76 @@ class MockPlexMediaItem: """Initialize the object.""" self.title = str(title) self.type = mediatype + self.thumbUrl = "http://1.2.3.4/thumb.png" + self._children = [] - def album(self, album): - """Mock the album lookup method.""" - return MockPlexMediaItem(album, mediatype="album") + def __iter__(self): + """Provide iterator.""" + yield from self._children - def track(self, track): - """Mock the track lookup method.""" - return MockPlexMediaTrack() + @property + def ratingKey(self): + """Mock the ratingKey property.""" + return id(self.title) - def tracks(self): - """Mock the tracks lookup method.""" - for index in range(1, 10): - yield MockPlexMediaTrack(index) - def episode(self, episode): - """Mock the episode lookup method.""" - return MockPlexMediaItem(episode, mediatype="episode") +class MockPlexPlaylist(MockPlexMediaItem): + """Mock a Plex Playlist instance.""" + + def __init__(self, items): + """Initialize the object.""" + super().__init__(f"Playlist ({len(items)} Items)", "playlist") + for item in items: + self._children.append(item) + + +class MockPlexShow(MockPlexMediaItem): + """Mock a Plex Show instance.""" + + def __init__(self, show): + """Initialize the object.""" + super().__init__(show, "show") + for index in range(1, 5): + self._children.append(MockPlexSeason(index)) def season(self, season): """Mock the season lookup method.""" - return MockPlexMediaItem(season, mediatype="season") + return [x for x in self._children if x.title == f"Season {season}"][0] + + +class MockPlexSeason(MockPlexMediaItem): + """Mock a Plex Season instance.""" + + def __init__(self, season): + """Initialize the object.""" + super().__init__(f"Season {season}", "season") + for index in range(1, 10): + self._children.append(MockPlexMediaItem(f"Episode {index}", "episode")) + + def episode(self, episode): + """Mock the episode lookup method.""" + return self._children[episode - 1] + + +class MockPlexAlbum(MockPlexMediaItem): + """Mock a Plex Album instance.""" + + def __init__(self, album): + """Initialize the object.""" + super().__init__(album, "album") + for index in range(1, 10): + self._children.append(MockPlexMediaTrack(index)) + + def track(self, track): + """Mock the track lookup method.""" + try: + return [x for x in self._children if x.title == track][0] + except IndexError: + raise NotFound + + def tracks(self): + """Mock the tracks lookup method.""" + return self._children class MockPlexArtist(MockPlexMediaItem): @@ -359,12 +485,16 @@ class MockPlexArtist(MockPlexMediaItem): def __init__(self, artist): """Initialize the object.""" - super().__init__(artist) - self.type = "artist" + super().__init__(artist, "artist") + self._album = MockPlexAlbum("Album") + + def album(self, album): + """Mock the album lookup method.""" + return self._album def get(self, track): """Mock the track lookup method.""" - return MockPlexMediaTrack() + return self._album.track(track) class MockPlexMediaTrack(MockPlexMediaItem): diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py new file mode 100644 index 00000000000..005fe4c9e44 --- /dev/null +++ b/tests/components/plex/test_browse_media.py @@ -0,0 +1,165 @@ +"""Tests for Plex media browser.""" +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, +) +from homeassistant.components.plex.const import CONF_SERVER_IDENTIFIER, DOMAIN +from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE_RESULT + +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 def test_browse_media(hass, hass_ws_client): + """Test getting Plex clients from plex.tv.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + mock_plex_account = MockPlexAccount() + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account + ), 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() + + websocket_client = await hass_ws_client(hass) + + trigger_plex_update(mock_websocket) + await hass.async_block_till_done() + + media_players = hass.states.async_entity_ids("media_player") + msg_id = 1 + + # Browse base of non-existent Plex server + await websocket_client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": media_players[0], + ATTR_MEDIA_CONTENT_TYPE: "server", + ATTR_MEDIA_CONTENT_ID: "this server does not exist", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == msg_id + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == ERR_UNKNOWN_ERROR + + # Browse base of Plex server + msg_id += 1 + await websocket_client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": media_players[0], + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == msg_id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + result = msg["result"] + assert result[ATTR_MEDIA_CONTENT_TYPE] == "server" + assert result[ATTR_MEDIA_CONTENT_ID] == DEFAULT_DATA[CONF_SERVER_IDENTIFIER] + assert len(result["children"]) == len(mock_plex_server.library.sections()) + + tvshows = next(iter(x for x in result["children"] if x["title"] == "TV Shows")) + playlists = next(iter(x for x in result["children"] if x["title"] == "Playlists")) + + # Browse into a Plex TV show library + msg_id += 1 + await websocket_client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": media_players[0], + ATTR_MEDIA_CONTENT_TYPE: tvshows[ATTR_MEDIA_CONTENT_TYPE], + ATTR_MEDIA_CONTENT_ID: str(tvshows[ATTR_MEDIA_CONTENT_ID]), + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == msg_id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + result = msg["result"] + assert result[ATTR_MEDIA_CONTENT_TYPE] == "library" + result_id = result[ATTR_MEDIA_CONTENT_ID] + assert len(result["children"]) == len( + mock_plex_server.library.sectionByID(result_id).all() + ) + + # Browse into a Plex TV show + msg_id += 1 + await websocket_client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": media_players[0], + ATTR_MEDIA_CONTENT_TYPE: result["children"][0][ATTR_MEDIA_CONTENT_TYPE], + ATTR_MEDIA_CONTENT_ID: str(result["children"][0][ATTR_MEDIA_CONTENT_ID]), + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == msg_id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + result = msg["result"] + assert result[ATTR_MEDIA_CONTENT_TYPE] == "show" + result_id = int(result[ATTR_MEDIA_CONTENT_ID]) + assert result["title"] == mock_plex_server.fetchItem(result_id).title + + # Browse into a non-existent TV season + msg_id += 1 + await websocket_client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": media_players[0], + ATTR_MEDIA_CONTENT_TYPE: result["children"][0][ATTR_MEDIA_CONTENT_TYPE], + ATTR_MEDIA_CONTENT_ID: str(99999999999999), + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == msg_id + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == ERR_UNKNOWN_ERROR + + # Browse Plex playlists + msg_id += 1 + await websocket_client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": media_players[0], + ATTR_MEDIA_CONTENT_TYPE: playlists[ATTR_MEDIA_CONTENT_TYPE], + ATTR_MEDIA_CONTENT_ID: str(playlists[ATTR_MEDIA_CONTENT_ID]), + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == msg_id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + result = msg["result"] + assert result[ATTR_MEDIA_CONTENT_TYPE] == "playlists" + result_id = result[ATTR_MEDIA_CONTENT_ID] diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 7da20846599..d911b258635 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -28,11 +28,13 @@ from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .helpers import trigger_plex_update from .mock_classes import ( MockPlexAccount, + MockPlexAlbum, MockPlexArtist, MockPlexLibrary, MockPlexLibrarySection, - MockPlexMediaItem, + MockPlexSeason, MockPlexServer, + MockPlexShow, ) from tests.async_mock import patch @@ -298,7 +300,7 @@ async def test_media_lookups(hass): with patch.object(MockPlexLibrary, "section", side_effect=NotFound): assert ( loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="A TV Show" + MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="TV Show" ) is None ) @@ -316,37 +318,37 @@ async def test_media_lookups(hass): is None ) assert loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="A TV Show" + MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="TV Show" ) assert loaded_server.lookup_media( MEDIA_TYPE_EPISODE, library_name="TV Shows", - show_name="A TV Show", + show_name="TV Show", season_number=2, ) assert loaded_server.lookup_media( MEDIA_TYPE_EPISODE, library_name="TV Shows", - show_name="A TV Show", + show_name="TV Show", season_number=2, episode_number=3, ) - with patch.object(MockPlexMediaItem, "season", side_effect=NotFound): + with patch.object(MockPlexShow, "season", side_effect=NotFound): assert ( loaded_server.lookup_media( MEDIA_TYPE_EPISODE, library_name="TV Shows", - show_name="A TV Show", + show_name="TV Show", season_number=2, ) is None ) - with patch.object(MockPlexMediaItem, "episode", side_effect=NotFound): + with patch.object(MockPlexSeason, "episode", side_effect=NotFound): assert ( loaded_server.lookup_media( MEDIA_TYPE_EPISODE, library_name="TV Shows", - show_name="A TV Show", + show_name="TV Show", season_number=2, episode_number=1, ) @@ -356,24 +358,24 @@ async def test_media_lookups(hass): # Music searches assert ( loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, library_name="Music", album_name="An Album" + MEDIA_TYPE_MUSIC, library_name="Music", album_name="Album" ) is None ) assert loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, library_name="Music", artist_name="An Artist" + MEDIA_TYPE_MUSIC, library_name="Music", artist_name="Artist" ) assert loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", - artist_name="An Artist", - track_name="A Track", + artist_name="Artist", + track_name="Track 3", ) assert loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", - artist_name="An Artist", - album_name="An Album", + artist_name="Artist", + album_name="Album", ) with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): assert ( @@ -381,7 +383,7 @@ async def test_media_lookups(hass): MEDIA_TYPE_MUSIC, library_name="Music", artist_name="Not an Artist", - album_name="An Album", + album_name="Album", ) is None ) @@ -390,18 +392,18 @@ async def test_media_lookups(hass): loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", - artist_name="An Artist", + artist_name="Artist", album_name="Not an Album", ) is None ) - with patch.object(MockPlexMediaItem, "track", side_effect=NotFound): + with patch.object(MockPlexAlbum, "track", side_effect=NotFound): assert ( loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", - artist_name="An Artist", - album_name="An Album", + artist_name="Artist", + album_name=" Album", track_name="Not a Track", ) is None @@ -411,7 +413,7 @@ async def test_media_lookups(hass): loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", - artist_name="An Artist", + artist_name="Artist", track_name="Not a Track", ) is None @@ -419,16 +421,16 @@ async def test_media_lookups(hass): assert loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", - artist_name="An Artist", - album_name="An Album", + artist_name="Artist", + album_name="Album", track_number=3, ) assert ( loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", - artist_name="An Artist", - album_name="An Album", + artist_name="Artist", + album_name="Album", track_number=30, ) is None @@ -436,9 +438,9 @@ async def test_media_lookups(hass): assert loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", - artist_name="An Artist", - album_name="An Album", - track_name="A Track", + artist_name="Artist", + album_name="Album", + track_name="Track 3", ) # Playlist searches @@ -453,10 +455,10 @@ async def test_media_lookups(hass): ) # Movie searches - assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, video_name="A Movie") is None + assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, video_name="Movie") is None assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, library_name="Movies") is None assert loaded_server.lookup_media( - MEDIA_TYPE_VIDEO, library_name="Movies", video_name="A Movie" + MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Movie" ) with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): assert ( From 634888473559a0d78ab3c6cf7383e58f24398cca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Aug 2020 23:37:33 +0200 Subject: [PATCH 365/862] Allow passing in user id instead of username to change password (#39266) --- .../config/auth_provider_homeassistant.py | 85 +++++++++---------- .../test_auth_provider_homeassistant.py | 39 +++++++-- 2 files changed, 75 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 696d215f68b..44ac6f23e2d 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -34,29 +34,21 @@ async def websocket_create(hass, connection, msg): user = await hass.auth.async_get_user(msg["user_id"]) if user is None: - connection.send_message( - websocket_api.error_message(msg["id"], "not_found", "User not found") - ) + connection.send_error(msg["id"], "not_found", "User not found") return if user.system_generated: - connection.send_message( - websocket_api.error_message( - msg["id"], - "system_generated", - "Cannot add credentials to a system generated user.", - ) + connection.send_error( + msg["id"], + "system_generated", + "Cannot add credentials to a system generated user.", ) return try: await provider.async_add_auth(msg["username"], msg["password"]) except auth_ha.InvalidUser: - connection.send_message( - websocket_api.error_message( - msg["id"], "username_exists", "Username already exists" - ) - ) + connection.send_error(msg["id"], "username_exists", "Username already exists") return credentials = await provider.async_get_or_create_credentials( @@ -64,7 +56,7 @@ async def websocket_create(hass, connection, msg): ) await hass.auth.async_link_user(user, credentials) - connection.send_message(websocket_api.result_message(msg["id"])) + connection.send_result(msg["id"]) @decorators.websocket_command( @@ -87,20 +79,18 @@ async def websocket_delete(hass, connection, msg): if not credentials.is_new: await hass.auth.async_remove_credentials(credentials) - connection.send_message(websocket_api.result_message(msg["id"])) + connection.send_result(msg["id"]) return try: await provider.async_remove_auth(msg["username"]) except auth_ha.InvalidUser: - connection.send_message( - websocket_api.error_message( - msg["id"], "auth_not_found", "Given username was not found." - ) + connection.send_error( + msg["id"], "auth_not_found", "Given username was not found." ) return - connection.send_message(websocket_api.result_message(msg["id"])) + connection.send_result(msg["id"]) @decorators.websocket_command( @@ -115,9 +105,7 @@ async def websocket_change_password(hass, connection, msg): """Change current user password.""" user = connection.user if user is None: - connection.send_message( - websocket_api.error_message(msg["id"], "user_not_found", "User not found") - ) + connection.send_error(msg["id"], "user_not_found", "User not found") return provider = auth_ha.async_get_provider(hass) @@ -128,26 +116,20 @@ async def websocket_change_password(hass, connection, msg): break if username is None: - connection.send_message( - websocket_api.error_message( - msg["id"], "credentials_not_found", "Credentials not found" - ) + connection.send_error( + msg["id"], "credentials_not_found", "Credentials not found" ) return try: await provider.async_validate_login(username, msg["current_password"]) except auth_ha.InvalidAuth: - connection.send_message( - websocket_api.error_message( - msg["id"], "invalid_password", "Invalid password" - ) - ) + connection.send_error(msg["id"], "invalid_password", "Invalid password") return await provider.async_change_password(username, msg["new_password"]) - connection.send_message(websocket_api.result_message(msg["id"])) + connection.send_result(msg["id"]) @decorators.websocket_command( @@ -155,7 +137,7 @@ async def websocket_change_password(hass, connection, msg): vol.Required( "type" ): "config/auth_provider/homeassistant/admin_change_password", - vol.Required("username"): str, + vol.Required("user_id"): str, vol.Required("password"): str, } ) @@ -166,14 +148,31 @@ async def websocket_admin_change_password(hass, connection, msg): if not connection.user.is_owner: raise Unauthorized(context=connection.context(msg)) + user = await hass.auth.async_get_user(msg["user_id"]) + + if user is None: + connection.send_error(msg["id"], "user_not_found", "User not found") + return + provider = auth_ha.async_get_provider(hass) - try: - await provider.async_change_password(msg["username"], msg["password"]) - connection.send_message(websocket_api.result_message(msg["id"])) - except auth_ha.InvalidUser: - connection.send_message( - websocket_api.error_message( - msg["id"], "credentials_not_found", "Credentials not found" - ) + + username = None + for credential in user.credentials: + if credential.auth_provider_type == provider.type: + username = credential.data["username"] + break + + if username is None: + connection.send_error( + msg["id"], "credentials_not_found", "Credentials not found" + ) + return + + try: + await provider.async_change_password(username, msg["password"]) + connection.send_result(msg["id"]) + except auth_ha.InvalidUser: + connection.send_error( + msg["id"], "credentials_not_found", "Credentials not found" ) return diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py index ab0682e8eaa..19568ff450b 100644 --- a/tests/components/config/test_auth_provider_homeassistant.py +++ b/tests/components/config/test_auth_provider_homeassistant.py @@ -323,7 +323,7 @@ async def test_admin_change_password_not_owner( { "id": 6, "type": "config/auth_provider/homeassistant/admin_change_password", - "username": "test-user", + "user_id": "test-user", "password": "new-pass", } ) @@ -336,15 +336,35 @@ async def test_admin_change_password_not_owner( await auth_provider.async_validate_login("test-user", "test-pass") -async def test_admin_change_password_no_creds(hass, hass_ws_client, owner_access_token): - """Test that change password fails with unknown credentials.""" +async def test_admin_change_password_no_user(hass, hass_ws_client, owner_access_token): + """Test that change password fails with unknown user.""" client = await hass_ws_client(hass, owner_access_token) await client.send_json( { "id": 6, "type": "config/auth_provider/homeassistant/admin_change_password", - "username": "non-existing", + "user_id": "non-existing", + "password": "new-pass", + } + ) + + result = await client.receive_json() + assert not result["success"], result + assert result["error"]["code"] == "user_not_found" + + +async def test_admin_change_password_no_cred( + hass, hass_ws_client, owner_access_token, hass_admin_user +): + """Test that change password fails with unknown credential.""" + client = await hass_ws_client(hass, owner_access_token) + + await client.send_json( + { + "id": 6, + "type": "config/auth_provider/homeassistant/admin_change_password", + "user_id": hass_admin_user.id, "password": "new-pass", } ) @@ -355,16 +375,23 @@ async def test_admin_change_password_no_creds(hass, hass_ws_client, owner_access async def test_admin_change_password( - hass, hass_ws_client, owner_access_token, auth_provider, test_user_credential + hass, + hass_ws_client, + owner_access_token, + auth_provider, + test_user_credential, + hass_admin_user, ): """Test that owners can change any password.""" + await hass.auth.async_link_user(hass_admin_user, test_user_credential) + client = await hass_ws_client(hass, owner_access_token) await client.send_json( { "id": 6, "type": "config/auth_provider/homeassistant/admin_change_password", - "username": "test-user", + "user_id": hass_admin_user.id, "password": "new-pass", } ) From 195d4b6897c430ceadde09133f2759120ce458ab Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 27 Aug 2020 00:04:55 +0000 Subject: [PATCH 366/862] [ci skip] Translation update --- .../accuweather/translations/ca.json | 2 +- .../accuweather/translations/ko.json | 9 ++ .../accuweather/translations/no.json | 3 +- .../accuweather/translations/pl.json | 2 +- .../accuweather/translations/pt-BR.json | 24 ++++ .../accuweather/translations/ru.json | 2 +- .../accuweather/translations/sensor.ca.json | 2 +- .../accuweather/translations/sensor.lb.json | 9 ++ .../translations/sensor.zh-Hant.json | 9 ++ .../accuweather/translations/zh-Hant.json | 2 +- .../components/acmeda/translations/no.json | 3 +- .../components/adguard/translations/no.json | 1 - .../adguard/translations/pt-BR.json | 2 +- .../components/agent_dvr/translations/no.json | 3 +- .../agent_dvr/translations/pt-BR.json | 7 ++ .../components/airly/translations/no.json | 3 +- .../components/airly/translations/pt.json | 4 +- .../components/airvisual/translations/no.json | 1 - .../airvisual/translations/pt-BR.json | 5 + .../alarm_control_panel/translations/cs.json | 2 +- .../components/almond/translations/no.json | 3 +- .../components/arcam_fmj/translations/no.json | 3 +- .../components/atag/translations/ca.json | 2 +- .../components/atag/translations/nl.json | 3 +- .../components/atag/translations/no.json | 6 +- .../components/atag/translations/pt-BR.json | 3 + .../components/auth/translations/et.json | 7 -- .../components/auth/translations/no.json | 3 +- .../components/avri/translations/no.json | 6 +- .../components/axis/translations/nl.json | 2 +- .../components/axis/translations/no.json | 1 - .../components/axis/translations/pt-BR.json | 2 +- .../azure_devops/translations/pt-BR.json | 23 ++++ .../azure_devops/translations/ru.json | 2 +- .../binary_sensor/translations/nb.json | 5 - .../binary_sensor/translations/no.json | 6 - .../components/blebox/translations/no.json | 3 +- .../components/blebox/translations/pt-BR.json | 3 +- .../components/blink/translations/ca.json | 4 +- .../components/blink/translations/es.json | 2 + .../components/blink/translations/it.json | 4 +- .../components/blink/translations/ko.json | 2 +- .../components/blink/translations/lb.json | 2 + .../components/blink/translations/ru.json | 4 +- .../blink/translations/zh-Hant.json | 4 +- .../components/bond/translations/ca.json | 3 +- .../components/bond/translations/en.json | 2 +- .../components/bond/translations/es.json | 1 + .../components/bond/translations/it.json | 1 + .../components/bond/translations/lb.json | 11 ++ .../components/bond/translations/no.json | 1 + .../components/bond/translations/pt-BR.json | 5 + .../components/bond/translations/ru.json | 5 +- .../components/bond/translations/zh-Hant.json | 1 + .../components/braviatv/translations/no.json | 3 +- .../components/broadlink/translations/ca.json | 46 +++++++ .../components/broadlink/translations/en.json | 86 ++++++------- .../components/broadlink/translations/es.json | 46 +++++++ .../components/broadlink/translations/fr.json | 22 ++++ .../components/broadlink/translations/it.json | 46 +++++++ .../components/broadlink/translations/lb.json | 45 +++++++ .../components/broadlink/translations/no.json | 46 +++++++ .../components/broadlink/translations/ru.json | 46 +++++++ .../broadlink/translations/zh-Hant.json | 46 +++++++ .../brother/translations/zh-Hant.json | 2 +- .../components/bsblan/translations/no.json | 3 +- .../cert_expiry/translations/no.json | 3 +- .../components/climate/translations/no.json | 1 - .../control4/translations/pt-BR.json | 15 +++ .../components/control4/translations/ru.json | 6 +- .../components/cover/translations/fr.json | 6 +- .../components/cover/translations/lb.json | 3 +- .../components/daikin/translations/ca.json | 3 +- .../components/daikin/translations/en.json | 3 +- .../components/daikin/translations/es.json | 3 +- .../components/daikin/translations/it.json | 3 +- .../components/daikin/translations/ko.json | 3 +- .../components/daikin/translations/lb.json | 3 +- .../components/daikin/translations/no.json | 3 +- .../components/daikin/translations/pt-BR.json | 3 +- .../components/daikin/translations/ru.json | 5 +- .../daikin/translations/zh-Hans.json | 3 +- .../daikin/translations/zh-Hant.json | 3 +- .../components/deconz/translations/no.json | 9 +- .../components/deconz/translations/pt-BR.json | 5 + .../components/demo/translations/no.json | 3 +- .../components/denonavr/translations/ru.json | 2 +- .../devolo_home_control/translations/no.json | 6 +- .../translations/pt-BR.json} | 4 +- .../components/dexcom/translations/no.json | 1 - .../components/dexcom/translations/ru.json | 2 +- .../components/directv/translations/no.json | 1 - .../components/directv/translations/ru.json | 4 +- .../components/doorbird/translations/ru.json | 2 +- .../components/dunehd/translations/no.json | 3 +- .../components/dunehd/translations/ru.json | 6 +- .../components/eafm/translations/ca.json | 17 +++ .../components/eafm/translations/en.json | 16 +-- .../components/eafm/translations/es.json | 17 +++ .../components/eafm/translations/it.json | 17 +++ .../components/eafm/translations/lb.json | 17 +++ .../components/eafm/translations/no.json | 17 +++ .../components/eafm/translations/ru.json | 17 +++ .../components/eafm/translations/zh-Hant.json | 17 +++ .../components/elgato/translations/no.json | 3 +- .../components/elgato/translations/pt-BR.json | 5 + .../elgato/translations/zh-Hant.json | 2 +- .../pt.json => elkm1/translations/pt-BR.json} | 8 +- .../emulated_roku/translations/et.json | 4 +- .../components/enocean/translations/no.json | 3 +- .../components/esphome/translations/no.json | 3 +- .../flick_electric/translations/pt-BR.json | 4 +- .../components/flo/translations/ca.json | 22 ++++ .../components/flo/translations/en.json | 14 +-- .../components/flo/translations/es.json | 22 ++++ .../components/flo/translations/it.json | 22 ++++ .../components/flo/translations/lb.json | 22 ++++ .../components/flo/translations/no.json | 22 ++++ .../components/flo/translations/pt-BR.json | 7 ++ .../components/flo/translations/ru.json | 22 ++++ .../components/flo/translations/zh-Hant.json | 22 ++++ .../forked_daapd/translations/nl.json | 2 +- .../forked_daapd/translations/no.json | 3 +- .../components/freebox/translations/no.json | 6 +- .../components/fritzbox/translations/fr.json | 2 +- .../fritzbox/translations/pt-BR.json | 10 ++ .../garmin_connect/translations/no.json | 3 +- .../components/gdacs/translations/no.json | 3 - .../geonetnz_quakes/translations/no.json | 4 - .../geonetnz_volcano/translations/no.json | 3 - .../components/gios/translations/no.json | 3 +- .../components/glances/translations/no.json | 1 - .../glances/translations/pt-BR.json | 4 +- .../components/gogogate2/translations/ru.json | 4 +- .../components/group/translations/nb.json | 1 - .../components/group/translations/no.json | 2 - .../components/guardian/translations/no.json | 3 +- .../components/hassio/translations/nb.json | 3 - .../components/hassio/translations/no.json | 3 - .../components/hlk_sw16/translations/it.json | 21 ++++ .../components/hlk_sw16/translations/lb.json | 21 ++++ .../components/hlk_sw16/translations/ru.json | 4 +- .../hlk_sw16/translations/zh-Hant.json | 21 ++++ .../homeassistant/translations/nb.json | 3 - .../homeassistant/translations/no.json | 3 - .../homeassistant/translations/pt.json | 3 - .../homekit_controller/translations/ca.json | 17 +++ .../homekit_controller/translations/en.json | 101 ++++++++------- .../homekit_controller/translations/es.json | 17 +++ .../homekit_controller/translations/fr.json | 4 + .../homekit_controller/translations/it.json | 17 +++ .../homekit_controller/translations/ko.json | 12 ++ .../homekit_controller/translations/lb.json | 17 +++ .../homekit_controller/translations/no.json | 17 +++ .../translations/pt-BR.json | 6 + .../homekit_controller/translations/ru.json | 17 +++ .../translations/zh-Hans.json | 12 ++ .../translations/zh-Hant.json | 17 +++ .../huawei_lte/translations/bg.json | 1 - .../huawei_lte/translations/cs.json | 1 - .../huawei_lte/translations/da.json | 1 - .../huawei_lte/translations/de.json | 1 - .../huawei_lte/translations/es-419.json | 1 - .../huawei_lte/translations/fr.json | 1 - .../huawei_lte/translations/hu.json | 1 - .../huawei_lte/translations/ko.json | 1 - .../huawei_lte/translations/lv.json | 1 - .../huawei_lte/translations/nl.json | 1 - .../huawei_lte/translations/no.json | 3 +- .../huawei_lte/translations/pl.json | 1 - .../huawei_lte/translations/pt-BR.json | 15 +++ .../huawei_lte/translations/pt.json | 1 - .../huawei_lte/translations/sl.json | 1 - .../huawei_lte/translations/sv.json | 1 - .../components/hue/translations/et.json | 7 -- .../components/hue/translations/no.json | 3 +- .../translations/pt-BR.json | 11 ++ .../iaqualink/translations/pt-BR.json | 11 ++ .../image_processing/translations/pt-BR.json | 2 +- .../input_boolean/translations/cs.json | 2 +- .../input_datetime/translations/cs.json | 2 +- .../input_number/translations/cs.json | 2 +- .../input_select/translations/cs.json | 2 +- .../input_text/translations/cs.json | 2 +- .../components/insteon/translations/ca.json | 115 ++++++++++++++++++ .../components/insteon/translations/es.json | 115 ++++++++++++++++++ .../components/insteon/translations/fr.json | 54 ++++++++ .../components/insteon/translations/it.json | 115 ++++++++++++++++++ .../components/insteon/translations/lb.json | 115 ++++++++++++++++++ .../components/insteon/translations/no.json | 115 ++++++++++++++++++ .../insteon/translations/pt-BR.json | 67 ++++++++++ .../components/insteon/translations/ru.json | 115 ++++++++++++++++++ .../insteon/translations/zh-Hant.json | 115 ++++++++++++++++++ .../components/ipp/translations/no.json | 1 - .../components/ipp/translations/pt-BR.json | 1 + .../components/ipp/translations/ru.json | 6 +- .../components/iqvia/translations/ca.json | 2 +- .../components/iqvia/translations/it.json | 3 + .../components/iqvia/translations/lb.json | 3 + .../components/iqvia/translations/no.json | 3 +- .../iqvia/translations/zh-Hant.json | 3 + .../components/isy994/translations/nl.json | 2 +- .../components/isy994/translations/pt-BR.json | 3 +- .../components/isy994/translations/ru.json | 4 +- .../components/kodi/translations/ca.json | 57 +++++++++ .../components/kodi/translations/en.json | 57 +++++++++ .../components/kodi/translations/es.json | 57 +++++++++ .../components/kodi/translations/fr.json | 24 ++++ .../components/kodi/translations/it.json | 57 +++++++++ .../components/kodi/translations/lb.json | 50 ++++++++ .../components/kodi/translations/no.json | 57 +++++++++ .../components/kodi/translations/ru.json | 57 +++++++++ .../components/kodi/translations/zh-Hant.json | 57 +++++++++ .../components/konnected/translations/ca.json | 7 +- .../components/konnected/translations/es.json | 1 + .../components/konnected/translations/fr.json | 3 +- .../components/konnected/translations/it.json | 7 +- .../components/konnected/translations/ko.json | 7 +- .../components/konnected/translations/lb.json | 1 + .../components/konnected/translations/no.json | 10 +- .../components/konnected/translations/ru.json | 7 +- .../konnected/translations/zh-Hant.json | 7 +- .../life360/translations/pt-BR.json | 2 +- .../components/lovelace/translations/nb.json | 3 - .../components/lovelace/translations/no.json | 3 - .../components/luftdaten/translations/no.json | 3 +- .../components/met/translations/no.json | 1 - .../components/met/translations/pt.json | 1 - .../meteo_france/translations/ca.json | 4 +- .../meteo_france/translations/lb.json | 19 +++ .../meteo_france/translations/no.json | 3 +- .../meteo_france/translations/pt.json | 3 +- .../meteo_france/translations/tr.json | 7 ++ .../components/metoffice/translations/ru.json | 4 +- .../components/mikrotik/translations/no.json | 1 - .../mikrotik/translations/pt-BR.json | 1 + .../components/mill/translations/ru.json | 2 +- .../components/monoprice/translations/no.json | 1 - .../components/mqtt/translations/it.json | 2 + .../components/mqtt/translations/lb.json | 2 + .../components/mqtt/translations/no.json | 2 - .../components/mqtt/translations/pt-BR.json | 2 +- .../components/mqtt/translations/zh-Hant.json | 2 + .../components/myq/translations/pt-BR.json | 11 ++ .../components/neato/translations/pt-BR.json | 11 ++ .../components/netatmo/translations/it.json | 3 +- .../components/netatmo/translations/lb.json | 3 +- .../netatmo/translations/zh-Hant.json | 3 +- .../components/nexia/translations/pt-BR.json | 11 ++ .../nightscout/translations/ca.json | 19 +++ .../nightscout/translations/en.json | 6 +- .../nightscout/translations/es.json | 19 +++ .../nightscout/translations/it.json | 19 +++ .../nightscout/translations/ko.json | 7 ++ .../nightscout/translations/lb.json | 19 +++ .../nightscout/translations/no.json | 19 +++ .../nightscout/translations/pt-BR.json | 18 +++ .../nightscout/translations/ru.json | 19 +++ .../nightscout/translations/tr.json | 14 +++ .../nightscout/translations/zh-Hant.json | 19 +++ .../components/notify/translations/pt-BR.json | 2 +- .../components/notion/translations/pt-BR.json | 2 +- .../components/nuheat/translations/pt-BR.json | 3 +- .../components/nut/translations/no.json | 2 - .../components/onvif/translations/no.json | 3 +- .../components/onvif/translations/pt-BR.json | 2 +- .../components/onvif/translations/tr.json | 7 ++ .../opentherm_gw/translations/no.json | 4 +- .../opentherm_gw/translations/pt.json | 1 - .../ovo_energy/translations/ca.json | 7 +- .../ovo_energy/translations/en.json | 10 +- .../ovo_energy/translations/es.json | 19 +++ .../ovo_energy/translations/it.json | 19 +++ .../ovo_energy/translations/ko.json | 12 ++ .../ovo_energy/translations/lb.json | 19 +++ .../ovo_energy/translations/no.json | 5 +- .../ovo_energy/translations/pt-BR.json | 7 ++ .../ovo_energy/translations/ru.json | 3 +- .../ovo_energy/translations/zh-Hant.json | 19 +++ .../panasonic_viera/translations/ca.json | 2 +- .../panasonic_viera/translations/no.json | 6 +- .../components/person/translations/nb.json | 3 +- .../components/person/translations/no.json | 3 +- .../components/pi_hole/translations/no.json | 1 - .../pi_hole/translations/pt-BR.json | 3 +- .../components/pi_hole/translations/ru.json | 2 +- .../components/plant/translations/nb.json | 1 - .../components/plant/translations/no.json | 6 - .../components/plex/translations/nl.json | 2 +- .../components/plex/translations/no.json | 10 +- .../components/plex/translations/pt-BR.json | 1 + .../components/plugwise/translations/ca.json | 2 +- .../components/plugwise/translations/no.json | 4 +- .../plum_lightpad/translations/ru.json | 2 +- .../components/poolsense/translations/no.json | 3 +- .../components/poolsense/translations/ru.json | 2 +- .../components/ps4/translations/no.json | 13 +- .../rainmachine/translations/no.json | 3 +- .../components/rfxtrx/translations/ca.json | 7 ++ .../components/rfxtrx/translations/en.json | 8 +- .../components/rfxtrx/translations/es.json | 7 ++ .../components/rfxtrx/translations/it.json | 17 +++ .../components/rfxtrx/translations/lb.json | 7 ++ .../components/rfxtrx/translations/no.json | 7 ++ .../components/rfxtrx/translations/ru.json | 7 ++ .../rfxtrx/translations/zh-Hant.json | 7 ++ .../components/ring/translations/ca.json | 2 +- .../components/ring/translations/pt-BR.json | 12 ++ .../components/risco/translations/ca.json | 33 +++++ .../components/risco/translations/en.json | 33 +++++ .../components/risco/translations/es.json | 33 +++++ .../components/risco/translations/fr.json | 31 +++++ .../components/risco/translations/it.json | 33 +++++ .../components/risco/translations/lb.json | 32 +++++ .../components/risco/translations/no.json | 33 +++++ .../components/risco/translations/ru.json | 33 +++++ .../risco/translations/zh-Hant.json | 33 +++++ .../components/roku/translations/no.json | 3 +- .../components/roku/translations/ru.json | 4 +- .../components/roon/translations/ca.json | 26 ++++ .../components/roon/translations/en.json | 8 +- .../components/roon/translations/es.json | 26 ++++ .../components/roon/translations/fr.json | 18 +++ .../components/roon/translations/it.json | 26 ++++ .../components/roon/translations/ko.json | 11 ++ .../components/roon/translations/lb.json | 26 ++++ .../components/roon/translations/nl.json | 26 ++++ .../components/roon/translations/no.json | 26 ++++ .../components/roon/translations/pt-BR.json | 26 ++++ .../components/roon/translations/ru.json | 26 ++++ .../components/roon/translations/zh-Hant.json | 26 ++++ .../components/samsungtv/translations/fr.json | 2 +- .../components/samsungtv/translations/no.json | 3 +- .../components/scene/translations/no.json | 3 - .../components/script/translations/no.json | 3 +- .../components/sensor/translations/ca.json | 12 +- .../components/sensor/translations/en.json | 12 +- .../sensor/translations/es-419.json | 6 - .../components/sensor/translations/es.json | 12 +- .../components/sensor/translations/it.json | 12 +- .../components/sensor/translations/lb.json | 12 +- .../components/sensor/translations/nb.json | 3 +- .../components/sensor/translations/no.json | 15 ++- .../components/sensor/translations/pt-BR.json | 8 ++ .../components/sensor/translations/ru.json | 12 +- .../sensor/translations/zh-Hant.json | 12 +- .../components/sentry/translations/ca.json | 22 +++- .../components/sentry/translations/en.json | 22 +++- .../components/sentry/translations/es.json | 22 +++- .../components/sentry/translations/fr.json | 17 +++ .../components/sentry/translations/it.json | 22 +++- .../components/sentry/translations/lb.json | 20 ++- .../components/sentry/translations/no.json | 25 +++- .../components/sentry/translations/ru.json | 22 +++- .../sentry/translations/zh-Hant.json | 22 +++- .../components/shelly/translations/ca.json | 24 ++++ .../components/shelly/translations/en.json | 10 +- .../components/shelly/translations/es.json | 24 ++++ .../components/shelly/translations/fr.json | 13 ++ .../components/shelly/translations/it.json | 24 ++++ .../components/shelly/translations/lb.json | 24 ++++ .../components/shelly/translations/no.json | 24 ++++ .../components/shelly/translations/pl.json | 24 ++++ .../components/shelly/translations/ru.json | 24 ++++ .../shelly/translations/zh-Hant.json | 24 ++++ .../simplisafe/translations/ca.json | 2 +- .../simplisafe/translations/es.json | 2 +- .../components/smappee/translations/ca.json | 21 ++++ .../components/smappee/translations/en.json | 32 ++--- .../components/smappee/translations/es.json | 21 ++++ .../components/smappee/translations/it.json | 21 ++++ .../components/smappee/translations/lb.json | 21 ++++ .../components/smappee/translations/no.json | 21 ++++ .../smappee/translations/pt-BR.json | 14 +++ .../components/smappee/translations/ru.json | 21 ++++ .../smappee/translations/zh-Hant.json | 21 ++++ .../smart_meter_texas/translations/ca.json | 22 ++++ .../smart_meter_texas/translations/en.json | 22 ++++ .../smart_meter_texas/translations/es.json | 22 ++++ .../smart_meter_texas/translations/fr.json | 10 ++ .../smart_meter_texas/translations/it.json | 22 ++++ .../smart_meter_texas/translations/ko.json | 22 ++++ .../smart_meter_texas/translations/lb.json | 22 ++++ .../smart_meter_texas/translations/no.json | 22 ++++ .../smart_meter_texas/translations/ru.json | 22 ++++ .../translations/zh-Hant.json | 22 ++++ .../components/sms/translations/ru.json | 4 +- .../components/soma/translations/no.json | 6 +- .../components/sonarr/translations/no.json | 5 +- .../components/sonarr/translations/ru.json | 2 +- .../components/songpal/translations/no.json | 1 - .../components/songpal/translations/ru.json | 4 +- .../components/spider/translations/ca.json | 3 +- .../components/spider/translations/it.json | 20 +++ .../components/spider/translations/lb.json | 20 +++ .../spider/translations/zh-Hant.json | 20 +++ .../squeezebox/translations/no.json | 5 +- .../squeezebox/translations/ru.json | 4 +- .../components/starline/translations/fr.json | 2 +- .../components/starline/translations/no.json | 4 +- .../starline/translations/pt-BR.json | 3 +- .../switch/translations/es-419.json | 6 - .../components/syncthru/translations/ru.json | 2 +- .../synology_dsm/translations/no.json | 8 +- .../components/tag/translations/ca.json | 3 + .../components/tag/translations/es.json | 3 + .../components/tag/translations/fr.json | 3 + .../components/tag/translations/it.json | 3 + .../components/tag/translations/lb.json | 3 + .../components/tag/translations/no.json | 3 + .../components/tag/translations/pt-BR.json | 3 + .../components/tag/translations/ru.json | 3 + .../components/tag/translations/zh-Hant.json | 3 + .../components/tibber/translations/no.json | 6 +- .../components/timer/translations/pt-BR.json | 4 +- .../components/toon/translations/ca.json | 4 +- .../totalconnect/translations/no.json | 3 +- .../totalconnect/translations/pt-BR.json | 3 + .../transmission/translations/no.json | 1 - .../components/tuya/translations/no.json | 3 +- .../components/tuya/translations/ru.json | 2 +- .../twentemilieu/translations/no.json | 3 +- .../components/unifi/translations/no.json | 1 - .../components/unifi/translations/ru.json | 2 +- .../components/upnp/translations/nl.json | 2 +- .../components/vacuum/translations/pt-BR.json | 2 +- .../components/vizio/translations/ca.json | 13 +- .../components/vizio/translations/en.json | 1 + .../components/vizio/translations/es.json | 1 + .../components/vizio/translations/it.json | 1 + .../components/vizio/translations/lb.json | 1 + .../components/vizio/translations/no.json | 1 + .../components/vizio/translations/pt-BR.json | 3 +- .../components/vizio/translations/ru.json | 5 +- .../vizio/translations/zh-Hant.json | 1 + .../components/volumio/translations/ru.json | 4 +- .../components/wilight/translations/ca.json | 16 +++ .../components/wilight/translations/en.json | 20 +-- .../components/wilight/translations/es.json | 16 +++ .../components/wilight/translations/fr.json | 14 +++ .../components/wilight/translations/it.json | 16 +++ .../components/wilight/translations/lb.json | 16 +++ .../components/wilight/translations/no.json | 16 +++ .../components/wilight/translations/ru.json | 16 +++ .../wilight/translations/zh-Hant.json | 16 +++ .../components/wolflink/translations/lb.json | 3 +- .../wolflink/translations/pt-BR.json | 20 +++ .../components/wolflink/translations/ru.json | 4 +- .../wolflink/translations/sensor.ko.json | 10 +- .../wolflink/translations/sensor.lb.json | 24 ++++ .../wolflink/translations/sensor.pt-BR.json | 18 +++ .../xiaomi_aqara/translations/ca.json | 8 +- .../xiaomi_aqara/translations/en.json | 9 +- .../xiaomi_aqara/translations/es.json | 6 +- .../xiaomi_aqara/translations/fy.json | 11 ++ .../xiaomi_aqara/translations/it.json | 8 +- .../xiaomi_aqara/translations/ko.json | 2 +- .../xiaomi_aqara/translations/lb.json | 6 +- .../xiaomi_aqara/translations/nl.json | 11 ++ .../xiaomi_aqara/translations/no.json | 13 +- .../xiaomi_aqara/translations/pl.json | 6 +- .../xiaomi_aqara/translations/pt.json | 5 - .../xiaomi_aqara/translations/ru.json | 10 +- .../xiaomi_aqara/translations/tr.json | 7 ++ .../xiaomi_aqara/translations/zh-Hant.json | 8 +- .../xiaomi_miio/translations/no.json | 3 +- .../xiaomi_miio/translations/pt-BR.json | 10 ++ .../xiaomi_miio/translations/ru.json | 4 +- .../components/zha/translations/ca.json | 1 + .../components/zha/translations/en.json | 1 + .../components/zha/translations/es.json | 1 + .../components/zha/translations/it.json | 1 + .../components/zha/translations/lb.json | 1 + .../components/zha/translations/no.json | 4 +- .../components/zha/translations/pt-BR.json | 6 + .../components/zha/translations/ru.json | 1 + .../components/zha/translations/zh-Hant.json | 1 + .../components/zone/translations/no.json | 3 +- 478 files changed, 5090 insertions(+), 664 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/ko.json create mode 100644 homeassistant/components/accuweather/translations/pt-BR.json create mode 100644 homeassistant/components/accuweather/translations/sensor.lb.json create mode 100644 homeassistant/components/accuweather/translations/sensor.zh-Hant.json delete mode 100644 homeassistant/components/auth/translations/et.json create mode 100644 homeassistant/components/azure_devops/translations/pt-BR.json create mode 100644 homeassistant/components/bond/translations/pt-BR.json create mode 100644 homeassistant/components/broadlink/translations/ca.json create mode 100644 homeassistant/components/broadlink/translations/es.json create mode 100644 homeassistant/components/broadlink/translations/fr.json create mode 100644 homeassistant/components/broadlink/translations/it.json create mode 100644 homeassistant/components/broadlink/translations/lb.json create mode 100644 homeassistant/components/broadlink/translations/no.json create mode 100644 homeassistant/components/broadlink/translations/ru.json create mode 100644 homeassistant/components/broadlink/translations/zh-Hant.json create mode 100644 homeassistant/components/control4/translations/pt-BR.json rename homeassistant/components/{tradfri/translations/et.json => devolo_home_control/translations/pt-BR.json} (64%) create mode 100644 homeassistant/components/eafm/translations/ca.json create mode 100644 homeassistant/components/eafm/translations/es.json create mode 100644 homeassistant/components/eafm/translations/it.json create mode 100644 homeassistant/components/eafm/translations/lb.json create mode 100644 homeassistant/components/eafm/translations/no.json create mode 100644 homeassistant/components/eafm/translations/ru.json create mode 100644 homeassistant/components/eafm/translations/zh-Hant.json rename homeassistant/components/{tibber/translations/pt.json => elkm1/translations/pt-BR.json} (50%) create mode 100644 homeassistant/components/flo/translations/ca.json create mode 100644 homeassistant/components/flo/translations/es.json create mode 100644 homeassistant/components/flo/translations/it.json create mode 100644 homeassistant/components/flo/translations/lb.json create mode 100644 homeassistant/components/flo/translations/no.json create mode 100644 homeassistant/components/flo/translations/pt-BR.json create mode 100644 homeassistant/components/flo/translations/ru.json create mode 100644 homeassistant/components/flo/translations/zh-Hant.json delete mode 100644 homeassistant/components/hassio/translations/nb.json delete mode 100644 homeassistant/components/hassio/translations/no.json create mode 100644 homeassistant/components/hlk_sw16/translations/it.json create mode 100644 homeassistant/components/hlk_sw16/translations/lb.json create mode 100644 homeassistant/components/hlk_sw16/translations/zh-Hant.json delete mode 100644 homeassistant/components/homeassistant/translations/nb.json delete mode 100644 homeassistant/components/homeassistant/translations/no.json delete mode 100644 homeassistant/components/homeassistant/translations/pt.json create mode 100644 homeassistant/components/huawei_lte/translations/pt-BR.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/pt-BR.json create mode 100644 homeassistant/components/iaqualink/translations/pt-BR.json create mode 100644 homeassistant/components/insteon/translations/ca.json create mode 100644 homeassistant/components/insteon/translations/es.json create mode 100644 homeassistant/components/insteon/translations/fr.json create mode 100644 homeassistant/components/insteon/translations/it.json create mode 100644 homeassistant/components/insteon/translations/lb.json create mode 100644 homeassistant/components/insteon/translations/no.json create mode 100644 homeassistant/components/insteon/translations/pt-BR.json create mode 100644 homeassistant/components/insteon/translations/ru.json create mode 100644 homeassistant/components/insteon/translations/zh-Hant.json create mode 100644 homeassistant/components/kodi/translations/ca.json create mode 100644 homeassistant/components/kodi/translations/en.json create mode 100644 homeassistant/components/kodi/translations/es.json create mode 100644 homeassistant/components/kodi/translations/fr.json create mode 100644 homeassistant/components/kodi/translations/it.json create mode 100644 homeassistant/components/kodi/translations/lb.json create mode 100644 homeassistant/components/kodi/translations/no.json create mode 100644 homeassistant/components/kodi/translations/ru.json create mode 100644 homeassistant/components/kodi/translations/zh-Hant.json delete mode 100644 homeassistant/components/lovelace/translations/nb.json delete mode 100644 homeassistant/components/lovelace/translations/no.json create mode 100644 homeassistant/components/meteo_france/translations/tr.json create mode 100644 homeassistant/components/myq/translations/pt-BR.json create mode 100644 homeassistant/components/neato/translations/pt-BR.json create mode 100644 homeassistant/components/nexia/translations/pt-BR.json create mode 100644 homeassistant/components/nightscout/translations/ca.json create mode 100644 homeassistant/components/nightscout/translations/es.json create mode 100644 homeassistant/components/nightscout/translations/it.json create mode 100644 homeassistant/components/nightscout/translations/ko.json create mode 100644 homeassistant/components/nightscout/translations/lb.json create mode 100644 homeassistant/components/nightscout/translations/no.json create mode 100644 homeassistant/components/nightscout/translations/pt-BR.json create mode 100644 homeassistant/components/nightscout/translations/ru.json create mode 100644 homeassistant/components/nightscout/translations/tr.json create mode 100644 homeassistant/components/nightscout/translations/zh-Hant.json create mode 100644 homeassistant/components/ovo_energy/translations/es.json create mode 100644 homeassistant/components/ovo_energy/translations/it.json create mode 100644 homeassistant/components/ovo_energy/translations/ko.json create mode 100644 homeassistant/components/ovo_energy/translations/lb.json create mode 100644 homeassistant/components/ovo_energy/translations/pt-BR.json create mode 100644 homeassistant/components/ovo_energy/translations/zh-Hant.json create mode 100644 homeassistant/components/rfxtrx/translations/ca.json create mode 100644 homeassistant/components/rfxtrx/translations/es.json create mode 100644 homeassistant/components/rfxtrx/translations/it.json create mode 100644 homeassistant/components/rfxtrx/translations/lb.json create mode 100644 homeassistant/components/rfxtrx/translations/no.json create mode 100644 homeassistant/components/rfxtrx/translations/ru.json create mode 100644 homeassistant/components/rfxtrx/translations/zh-Hant.json create mode 100644 homeassistant/components/ring/translations/pt-BR.json create mode 100644 homeassistant/components/risco/translations/ca.json create mode 100644 homeassistant/components/risco/translations/en.json create mode 100644 homeassistant/components/risco/translations/es.json create mode 100644 homeassistant/components/risco/translations/fr.json create mode 100644 homeassistant/components/risco/translations/it.json create mode 100644 homeassistant/components/risco/translations/lb.json create mode 100644 homeassistant/components/risco/translations/no.json create mode 100644 homeassistant/components/risco/translations/ru.json create mode 100644 homeassistant/components/risco/translations/zh-Hant.json create mode 100644 homeassistant/components/roon/translations/ca.json create mode 100644 homeassistant/components/roon/translations/es.json create mode 100644 homeassistant/components/roon/translations/fr.json create mode 100644 homeassistant/components/roon/translations/it.json create mode 100644 homeassistant/components/roon/translations/ko.json create mode 100644 homeassistant/components/roon/translations/lb.json create mode 100644 homeassistant/components/roon/translations/nl.json create mode 100644 homeassistant/components/roon/translations/no.json create mode 100644 homeassistant/components/roon/translations/pt-BR.json create mode 100644 homeassistant/components/roon/translations/ru.json create mode 100644 homeassistant/components/roon/translations/zh-Hant.json delete mode 100644 homeassistant/components/scene/translations/no.json create mode 100644 homeassistant/components/shelly/translations/ca.json create mode 100644 homeassistant/components/shelly/translations/es.json create mode 100644 homeassistant/components/shelly/translations/fr.json create mode 100644 homeassistant/components/shelly/translations/it.json create mode 100644 homeassistant/components/shelly/translations/lb.json create mode 100644 homeassistant/components/shelly/translations/no.json create mode 100644 homeassistant/components/shelly/translations/pl.json create mode 100644 homeassistant/components/shelly/translations/ru.json create mode 100644 homeassistant/components/shelly/translations/zh-Hant.json create mode 100644 homeassistant/components/smappee/translations/pt-BR.json create mode 100644 homeassistant/components/smart_meter_texas/translations/ca.json create mode 100644 homeassistant/components/smart_meter_texas/translations/en.json create mode 100644 homeassistant/components/smart_meter_texas/translations/es.json create mode 100644 homeassistant/components/smart_meter_texas/translations/fr.json create mode 100644 homeassistant/components/smart_meter_texas/translations/it.json create mode 100644 homeassistant/components/smart_meter_texas/translations/ko.json create mode 100644 homeassistant/components/smart_meter_texas/translations/lb.json create mode 100644 homeassistant/components/smart_meter_texas/translations/no.json create mode 100644 homeassistant/components/smart_meter_texas/translations/ru.json create mode 100644 homeassistant/components/smart_meter_texas/translations/zh-Hant.json create mode 100644 homeassistant/components/spider/translations/it.json create mode 100644 homeassistant/components/spider/translations/lb.json create mode 100644 homeassistant/components/spider/translations/zh-Hant.json create mode 100644 homeassistant/components/tag/translations/ca.json create mode 100644 homeassistant/components/tag/translations/es.json create mode 100644 homeassistant/components/tag/translations/fr.json create mode 100644 homeassistant/components/tag/translations/it.json create mode 100644 homeassistant/components/tag/translations/lb.json create mode 100644 homeassistant/components/tag/translations/no.json create mode 100644 homeassistant/components/tag/translations/pt-BR.json create mode 100644 homeassistant/components/tag/translations/ru.json create mode 100644 homeassistant/components/tag/translations/zh-Hant.json create mode 100644 homeassistant/components/wilight/translations/ca.json create mode 100644 homeassistant/components/wilight/translations/es.json create mode 100644 homeassistant/components/wilight/translations/fr.json create mode 100644 homeassistant/components/wilight/translations/it.json create mode 100644 homeassistant/components/wilight/translations/lb.json create mode 100644 homeassistant/components/wilight/translations/no.json create mode 100644 homeassistant/components/wilight/translations/ru.json create mode 100644 homeassistant/components/wilight/translations/zh-Hant.json create mode 100644 homeassistant/components/wolflink/translations/pt-BR.json create mode 100644 homeassistant/components/wolflink/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/fy.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/nl.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/tr.json diff --git a/homeassistant/components/accuweather/translations/ca.json b/homeassistant/components/accuweather/translations/ca.json index 72eea7f65cc..55e1bdc83b7 100644 --- a/homeassistant/components/accuweather/translations/ca.json +++ b/homeassistant/components/accuweather/translations/ca.json @@ -16,7 +16,7 @@ "longitude": "Longitud", "name": "Nom de la integraci\u00f3" }, - "description": "Si necessites ajuda amb la configuraci\u00f3, consulta: https://www.home-assistant.io/integrations/accuweather/ \n\n La previsi\u00f3 meteorol\u00f2gica no est\u00e0 habilitada de manera predeterminada. Pots activar-la en les opcions de la integraci\u00f3.", + "description": "Si necessites ajuda amb la configuraci\u00f3, consulta els seg\u00fcent enlla\u00e7: https://www.home-assistant.io/integrations/accuweather/ \n\n Alguns sensors no estan activats de manera predeterminada. Els pots activar des del registre d'entitats, despr\u00e9s de la configurraci\u00f3 de la integraci\u00f3.\n La previsi\u00f3 meteorol\u00f2gica no est\u00e0 activada de manera predeterminada. Pots activar-la en les opcions de la integraci\u00f3.", "title": "AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/ko.json b/homeassistant/components/accuweather/translations/ko.json new file mode 100644 index 00000000000..b04778c8cb2 --- /dev/null +++ b/homeassistant/components/accuweather/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694:\nhttps://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc5f0\ub3d9 \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/no.json b/homeassistant/components/accuweather/translations/no.json index c6cbc82bc2c..f0ab46267b2 100644 --- a/homeassistant/components/accuweather/translations/no.json +++ b/homeassistant/components/accuweather/translations/no.json @@ -16,8 +16,7 @@ "longitude": "Lengdegrad", "name": "Navn p\u00e5 integrasjon" }, - "description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\n Noen sensorer er ikke aktivert som standard. Du kan aktivere dem i enhetsregisteret etter integrasjonskonfigurasjonen. \n V\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene.", - "title": "" + "description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\n Noen sensorer er ikke aktivert som standard. Du kan aktivere dem i enhetsregisteret etter integrasjonskonfigurasjonen. \n V\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene." } } }, diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index 273458db00c..a518c287b11 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", "invalid_api_key": "Nieprawid\u0142owy klucz API.", - "requests_exceeded": "Dozwolona liczba zapyta\u0144 do interfejsu API Accuweather zosta\u0142a przekroczona. Musisz poczeka\u0107 lub zmieni\u0107 klucz API." + "requests_exceeded": "Dozwolona liczba zapyta\u0144 do interfejsu API AccuWeather zosta\u0142a przekroczona. Musisz poczeka\u0107 lub zmieni\u0107 klucz API." }, "step": { "user": { diff --git a/homeassistant/components/accuweather/translations/pt-BR.json b/homeassistant/components/accuweather/translations/pt-BR.json new file mode 100644 index 00000000000..75111f9892d --- /dev/null +++ b/homeassistant/components/accuweather/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave API", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Previs\u00e3o do Tempo" + }, + "description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave da API AccuWeather, quando voc\u00ea habilita a previs\u00e3o do tempo, as atualiza\u00e7\u00f5es de dados ser\u00e3o realizadas a cada 64 minutos em vez de a cada 32 minutos." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json index 8803659ccbb..16623e4e704 100644 --- a/homeassistant/components/accuweather/translations/ru.json +++ b/homeassistant/components/accuweather/translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", "requests_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043a API Accuweather. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u043e\u0434\u043e\u0436\u0434\u0430\u0442\u044c \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." }, diff --git a/homeassistant/components/accuweather/translations/sensor.ca.json b/homeassistant/components/accuweather/translations/sensor.ca.json index c2395047ccb..ad6c43a54ca 100644 --- a/homeassistant/components/accuweather/translations/sensor.ca.json +++ b/homeassistant/components/accuweather/translations/sensor.ca.json @@ -1,7 +1,7 @@ { "state": { "accuweather__pressure_tendency": { - "falling": "Caient", + "falling": "Disminuint", "rising": "Augmentant", "steady": "Estable" } diff --git a/homeassistant/components/accuweather/translations/sensor.lb.json b/homeassistant/components/accuweather/translations/sensor.lb.json new file mode 100644 index 00000000000..b4d90370e7c --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.lb.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "R\u00e9ckleefeg", + "rising": "Erh\u00e9ijung", + "steady": "Stabil" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.zh-Hant.json b/homeassistant/components/accuweather/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..35bc04eaf04 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.zh-Hant.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\u4e0b\u964d", + "rising": "\u4e0a\u5347", + "steady": "\u7a69\u5b9a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index d5f3acfc81c..9c544b033df 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -16,7 +16,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u6574\u5408\u540d\u7a31" }, - "description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002", + "description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u67d0\u4e9b\u50b3\u611f\u5668\u9810\u8a2d\u70ba\u672a\u555f\u7528\uff0c\u53ef\u4ee5\u65bc\u6574\u5408\u8a2d\u5b9a\u4e2d\u555f\u7528\u9019\u4e9b\u7269\u4ef6\u3002\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002", "title": "AccuWeather" } } diff --git a/homeassistant/components/acmeda/translations/no.json b/homeassistant/components/acmeda/translations/no.json index 66335077cfb..5364fc683eb 100644 --- a/homeassistant/components/acmeda/translations/no.json +++ b/homeassistant/components/acmeda/translations/no.json @@ -11,6 +11,5 @@ "title": "Velg en hub du vil legge til" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index a772988c042..725ca4a7a32 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -16,7 +16,6 @@ "data": { "host": "Vert", "password": "Passord", - "port": "", "ssl": "AdGuard Hjem bruker et SSL-sertifikat", "username": "Brukernavn", "verify_ssl": "AdGuard Home bruker et riktig sertifikat" diff --git a/homeassistant/components/adguard/translations/pt-BR.json b/homeassistant/components/adguard/translations/pt-BR.json index 942c793b9b4..57174a4cf3e 100644 --- a/homeassistant/components/adguard/translations/pt-BR.json +++ b/homeassistant/components/adguard/translations/pt-BR.json @@ -16,7 +16,7 @@ "data": { "password": "Senha", "ssl": "O AdGuard Home usa um certificado SSL", - "username": "Nome de usu\u00e1rio", + "username": "Usu\u00e1rio", "verify_ssl": "O AdGuard Home usa um certificado apropriado" }, "description": "Configure sua inst\u00e2ncia do AdGuard Home para permitir o monitoramento e o controle." diff --git a/homeassistant/components/agent_dvr/translations/no.json b/homeassistant/components/agent_dvr/translations/no.json index 3fcbb8f1617..247654fdde9 100644 --- a/homeassistant/components/agent_dvr/translations/no.json +++ b/homeassistant/components/agent_dvr/translations/no.json @@ -10,8 +10,7 @@ "step": { "user": { "data": { - "host": "Vert", - "port": "" + "host": "Vert" }, "title": "Konfigurere Agent DVR" } diff --git a/homeassistant/components/agent_dvr/translations/pt-BR.json b/homeassistant/components/agent_dvr/translations/pt-BR.json index df74434ffc7..713cdd8cd0b 100644 --- a/homeassistant/components/agent_dvr/translations/pt-BR.json +++ b/homeassistant/components/agent_dvr/translations/pt-BR.json @@ -2,6 +2,13 @@ "config": { "error": { "device_unavailable": "O dispositivo n\u00e3o est\u00e1 dispon\u00edvel" + }, + "step": { + "user": { + "data": { + "port": "Porta" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index 09e77a311eb..6222dddfe28 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -15,8 +15,7 @@ "longitude": "Lengdegrad", "name": "Navn p\u00e5 integrasjonen" }, - "description": "Sett opp Airly luftkvalitet integrasjon. For \u00e5 opprette API-n\u00f8kkel, g\u00e5 til [https://developer.airly.eu/register](https://developer.airly.eu/register)", - "title": "" + "description": "Sett opp Airly luftkvalitet integrasjon. For \u00e5 opprette API-n\u00f8kkel, g\u00e5 til [https://developer.airly.eu/register](https://developer.airly.eu/register)" } } } diff --git a/homeassistant/components/airly/translations/pt.json b/homeassistant/components/airly/translations/pt.json index ae35beabf6b..c7081cd694a 100644 --- a/homeassistant/components/airly/translations/pt.json +++ b/homeassistant/components/airly/translations/pt.json @@ -3,11 +3,9 @@ "step": { "user": { "data": { - "api_key": "", "latitude": "Latitude", "longitude": "Longitude" - }, - "title": "" + } } } } diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index 8fcf00a6714..b9be5498560 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -29,7 +29,6 @@ "user": { "data": { "cloud_api": "Geografisk plassering", - "node_pro": "", "type": "Integrasjonstype" }, "description": "Velg hvilken type AirVisual-data du vil overv\u00e5ke.", diff --git a/homeassistant/components/airvisual/translations/pt-BR.json b/homeassistant/components/airvisual/translations/pt-BR.json index 035782cb320..9f78c46b5e0 100644 --- a/homeassistant/components/airvisual/translations/pt-BR.json +++ b/homeassistant/components/airvisual/translations/pt-BR.json @@ -11,6 +11,11 @@ "longitude": "Longitude" } }, + "node_pro": { + "data": { + "password": "Senha" + } + }, "user": { "data": { "type": "Tipo de Integra\u00e7\u00e3o" diff --git a/homeassistant/components/alarm_control_panel/translations/cs.json b/homeassistant/components/alarm_control_panel/translations/cs.json index 40a6fd40338..66786dfc0e2 100644 --- a/homeassistant/components/alarm_control_panel/translations/cs.json +++ b/homeassistant/components/alarm_control_panel/translations/cs.json @@ -25,7 +25,7 @@ "state": { "_": { "armed": "Zabezpe\u010deno", - "armed_away": "Re\u017eim nep\u0159\u00edtomnost", + "armed_away": "Nep\u0159\u00edtomnost", "armed_custom_bypass": "Zabezpe\u010deno u\u017eivatelsk\u00fdm obejit\u00edm", "armed_home": "Re\u017eim domov", "armed_night": "No\u010dn\u00ed re\u017eim", diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 3a6a89a8340..6e5c90b69e2 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -7,8 +7,7 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?", - "title": "" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?" }, "pick_implementation": { "title": "Velg godkjenningsmetode" diff --git a/homeassistant/components/arcam_fmj/translations/no.json b/homeassistant/components/arcam_fmj/translations/no.json index 14d55224119..b067d24dc44 100644 --- a/homeassistant/components/arcam_fmj/translations/no.json +++ b/homeassistant/components/arcam_fmj/translations/no.json @@ -12,8 +12,7 @@ }, "user": { "data": { - "host": "Vert", - "port": "" + "host": "Vert" }, "description": "Vennligst skriv inn vertsnavnet eller IP-adressen til enheten." } diff --git a/homeassistant/components/atag/translations/ca.json b/homeassistant/components/atag/translations/ca.json index ac9959bca39..6677e7eaa4d 100644 --- a/homeassistant/components/atag/translations/ca.json +++ b/homeassistant/components/atag/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "connection_error": "No s'ha pogut connectar, torna-ho a provar", - "unauthorized": "L'emparellament s'ha denegat, comprova si hi ha una sol\u00b7licitud d'autenticaci\u00f3 al dispositiu" + "unauthorized": "La vinculaci\u00f3 s'ha denegat, comprova si hi ha una sol\u00b7licitud d'autenticaci\u00f3 al dispositiu" }, "step": { "user": { diff --git a/homeassistant/components/atag/translations/nl.json b/homeassistant/components/atag/translations/nl.json index 077beb65871..ac6477ec4d2 100644 --- a/homeassistant/components/atag/translations/nl.json +++ b/homeassistant/components/atag/translations/nl.json @@ -16,6 +16,5 @@ "title": "Verbinding maken met het apparaat" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/atag/translations/no.json b/homeassistant/components/atag/translations/no.json index a0e428f286a..aa2f7d1b3b8 100644 --- a/homeassistant/components/atag/translations/no.json +++ b/homeassistant/components/atag/translations/no.json @@ -11,12 +11,10 @@ "user": { "data": { "email": "E-post (valgfritt)", - "host": "Vert", - "port": "" + "host": "Vert" }, "title": "Koble til enheten" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/atag/translations/pt-BR.json b/homeassistant/components/atag/translations/pt-BR.json index eda0a2cc041..b4bba5ea4d1 100644 --- a/homeassistant/components/atag/translations/pt-BR.json +++ b/homeassistant/components/atag/translations/pt-BR.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Este dispositivo j\u00e1 foi adicionado ao Home Assistant" + }, "error": { "connection_error": "Falha ao conectar, tente novamente" }, diff --git a/homeassistant/components/auth/translations/et.json b/homeassistant/components/auth/translations/et.json deleted file mode 100644 index 290f4ee12a9..00000000000 --- a/homeassistant/components/auth/translations/et.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mfa_setup": { - "totp": { - "title": "" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/no.json b/homeassistant/components/auth/translations/no.json index ea0f1baa067..d19140ee218 100644 --- a/homeassistant/components/auth/translations/no.json +++ b/homeassistant/components/auth/translations/no.json @@ -28,8 +28,7 @@ "description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte engangspassord, skann QR-koden med autentiseringsappen din. Hvis du ikke har en, kan vi anbefale enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/).\n\n {qr_code} \n \nEtter at du har skannet koden, angir du den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du fylle inn f\u00f8lgende kode manuelt: **`{code}`**.", "title": "Sett opp tofaktorautentisering ved hjelp av TOTP" } - }, - "title": "" + } } } } \ No newline at end of file diff --git a/homeassistant/components/avri/translations/no.json b/homeassistant/components/avri/translations/no.json index 5d7f77113b9..4fb4490ac88 100644 --- a/homeassistant/components/avri/translations/no.json +++ b/homeassistant/components/avri/translations/no.json @@ -15,10 +15,8 @@ "house_number_extension": "Utvidelse av husnummer", "zip_code": "Postnummer" }, - "description": "Skriv inn adressen din", - "title": "" + "description": "Skriv inn adressen din" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/nl.json b/homeassistant/components/axis/translations/nl.json index 8919514e44a..01257e7585a 100644 --- a/homeassistant/components/axis/translations/nl.json +++ b/homeassistant/components/axis/translations/nl.json @@ -12,7 +12,7 @@ "device_unavailable": "Apparaat is niet beschikbaar", "faulty_credentials": "Ongeldige gebruikersreferenties" }, - "flow_title": "Axis apparaat: {naam} ({host})", + "flow_title": "Axis apparaat: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/no.json b/homeassistant/components/axis/translations/no.json index 039e6138753..70836d88239 100644 --- a/homeassistant/components/axis/translations/no.json +++ b/homeassistant/components/axis/translations/no.json @@ -18,7 +18,6 @@ "data": { "host": "Vert", "password": "Passord", - "port": "", "username": "Brukernavn" }, "title": "Sett opp Axis enhet" diff --git a/homeassistant/components/axis/translations/pt-BR.json b/homeassistant/components/axis/translations/pt-BR.json index c63bbf7ed0a..0c6ab9249df 100644 --- a/homeassistant/components/axis/translations/pt-BR.json +++ b/homeassistant/components/axis/translations/pt-BR.json @@ -19,7 +19,7 @@ "host": "Host", "password": "Senha", "port": "Porta", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" }, "title": "Configurar o dispositivo Axis" } diff --git a/homeassistant/components/azure_devops/translations/pt-BR.json b/homeassistant/components/azure_devops/translations/pt-BR.json new file mode 100644 index 00000000000..510159829cb --- /dev/null +++ b/homeassistant/components/azure_devops/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "project_error": "N\u00e3o foi poss\u00edvel obter informa\u00e7\u00f5es do projeto." + }, + "step": { + "reauth": { + "data": { + "personal_access_token": "Token de acesso pessoal (PAT)" + }, + "description": "A autentica\u00e7\u00e3o falhou para {project_url}. Por favor, insira suas credenciais atuais.", + "title": "Reautentica\u00e7\u00e3o" + }, + "user": { + "data": { + "organization": "Organiza\u00e7\u00e3o", + "personal_access_token": "Token de acesso pessoal (PAT)", + "project": "Projeto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/ru.json b/homeassistant/components/azure_devops/translations/ru.json index 08e07d68560..2a7956c5c90 100644 --- a/homeassistant/components/azure_devops/translations/ru.json +++ b/homeassistant/components/azure_devops/translations/ru.json @@ -25,7 +25,7 @@ "project": "\u041f\u0440\u043e\u0435\u043a\u0442" }, "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 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0430\u0441\u0442\u043d\u044b\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432.", - "title": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442 Azure DevOps" + "title": "Azure DevOps" } } }, diff --git a/homeassistant/components/binary_sensor/translations/nb.json b/homeassistant/components/binary_sensor/translations/nb.json index 76c56713646..8b143f7499a 100644 --- a/homeassistant/components/binary_sensor/translations/nb.json +++ b/homeassistant/components/binary_sensor/translations/nb.json @@ -9,7 +9,6 @@ "on": "Lavt" }, "cold": { - "off": "", "on": "Kald" }, "connectivity": { @@ -56,10 +55,6 @@ "off": "Borte", "on": "Hjemme" }, - "problem": { - "off": "", - "on": "" - }, "safety": { "off": "Sikker", "on": "Usikker" diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index b78a50a8628..25b7c165c11 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -99,7 +99,6 @@ "on": "Lavt" }, "cold": { - "off": "", "on": "Kald" }, "connectivity": { @@ -119,7 +118,6 @@ "on": "Oppdaget" }, "heat": { - "off": "", "on": "Varm" }, "lock": { @@ -146,10 +144,6 @@ "off": "Borte", "on": "Hjemme" }, - "problem": { - "off": "", - "on": "" - }, "safety": { "off": "Sikker", "on": "Usikker" diff --git a/homeassistant/components/blebox/translations/no.json b/homeassistant/components/blebox/translations/no.json index 239d1fb03c6..925f680107e 100644 --- a/homeassistant/components/blebox/translations/no.json +++ b/homeassistant/components/blebox/translations/no.json @@ -13,8 +13,7 @@ "step": { "user": { "data": { - "host": "IP adresse", - "port": "" + "host": "IP adresse" }, "description": "Konfigurer BleBox-en til \u00e5 integreres med Home Assistant.", "title": "Konfigurere BleBox-enheten" diff --git a/homeassistant/components/blebox/translations/pt-BR.json b/homeassistant/components/blebox/translations/pt-BR.json index f7dc708a2d6..972aed55cc4 100644 --- a/homeassistant/components/blebox/translations/pt-BR.json +++ b/homeassistant/components/blebox/translations/pt-BR.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "Endere\u00e7o IP" + "host": "Endere\u00e7o IP", + "port": "Porta" } } } diff --git a/homeassistant/components/blink/translations/ca.json b/homeassistant/components/blink/translations/ca.json index a79866fe614..bd5079d2a16 100644 --- a/homeassistant/components/blink/translations/ca.json +++ b/homeassistant/components/blink/translations/ca.json @@ -14,7 +14,7 @@ "data": { "2fa": "Codi de dos factors" }, - "description": "Introdueix el PIN que has rebut per correu electr\u00f2nic. Si el correu no cont\u00e9 cap PIN, deixa-ho en blanc.", + "description": "Introdueix el PIN que s'ha enviat al teu correu electr\u00f2nic", "title": "Autenticaci\u00f3 de dos factors" }, "user": { @@ -22,7 +22,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "title": "Inici de sessi\u00f3 amb Blink" + "title": "Inici de sessi\u00f3 amb un compte de Blink" } } }, diff --git a/homeassistant/components/blink/translations/es.json b/homeassistant/components/blink/translations/es.json index 91975d48d1f..1b25e1a52e2 100644 --- a/homeassistant/components/blink/translations/es.json +++ b/homeassistant/components/blink/translations/es.json @@ -4,6 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { + "cannot_connect": "No se pudo conectar", + "invalid_access_token": "Token de acceso no v\u00e1lido", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/blink/translations/it.json b/homeassistant/components/blink/translations/it.json index e7311549fae..bdb0ba3f6b4 100644 --- a/homeassistant/components/blink/translations/it.json +++ b/homeassistant/components/blink/translations/it.json @@ -4,6 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_access_token": "Token di accesso non valido", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, @@ -12,7 +14,7 @@ "data": { "2fa": "Codice a due fattori" }, - "description": "Inserisci il PIN inviato alla tua e-mail. Se l'e-mail non contiene un PIN, lasciare vuoto", + "description": "Inserisci il pin inviato alla tua email", "title": "Autenticazione a due fattori" }, "user": { diff --git a/homeassistant/components/blink/translations/ko.json b/homeassistant/components/blink/translations/ko.json index fb26625c7e3..ef3ffc108e5 100644 --- a/homeassistant/components/blink/translations/ko.json +++ b/homeassistant/components/blink/translations/ko.json @@ -12,7 +12,7 @@ "data": { "2fa": "2\ub2e8\uacc4 \uc778\uc99d \ucf54\ub4dc" }, - "description": "\uc774\uba54\uc77c\ub85c \ubcf4\ub0b4\ub4dc\ub9b0 PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc774\uba54\uc77c\uc5d0 PIN \uc774 \ud3ec\ud568\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \ube44\uc6cc \ub461\ub2c8\ub2e4", + "description": "\uc774\uba54\uc77c\ub85c \ubcf4\ub0b4\ub4dc\ub9b0 PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "2\ub2e8\uacc4 \uc778\uc99d" }, "user": { diff --git a/homeassistant/components/blink/translations/lb.json b/homeassistant/components/blink/translations/lb.json index 830f5364896..e77dfe9a38f 100644 --- a/homeassistant/components/blink/translations/lb.json +++ b/homeassistant/components/blink/translations/lb.json @@ -4,6 +4,8 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert" }, "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_access_token": "Ong\u00ebltege Acc\u00e8s Jeton", "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" }, diff --git a/homeassistant/components/blink/translations/ru.json b/homeassistant/components/blink/translations/ru.json index 72dcb497cec..0e55fa716b9 100644 --- a/homeassistant/components/blink/translations/ru.json +++ b/homeassistant/components/blink/translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/blink/translations/zh-Hant.json b/homeassistant/components/blink/translations/zh-Hant.json index 67234dc8841..5736c91714c 100644 --- a/homeassistant/components/blink/translations/zh-Hant.json +++ b/homeassistant/components/blink/translations/zh-Hant.json @@ -4,6 +4,8 @@ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, @@ -12,7 +14,7 @@ "data": { "2fa": "\u96d9\u91cd\u9a57\u8b49\u78bc" }, - "description": "\u8f38\u5165\u90f5\u4ef6\u6240\u6536\u5230 PIN \u78bc\uff0c\u5047\u5982\u90f5\u4ef6\u4e2d\u6c92\u6709 PIN \u78bc\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d", + "description": "\u8f38\u5165\u90f5\u4ef6\u6240\u6536\u5230 PIN \u78bc", "title": "\u96d9\u91cd\u9a57\u8b49" }, "user": { diff --git a/homeassistant/components/bond/translations/ca.json b/homeassistant/components/bond/translations/ca.json index 8cccca1afc8..3903ea77c34 100644 --- a/homeassistant/components/bond/translations/ca.json +++ b/homeassistant/components/bond/translations/ca.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "old_firmware": "Hi ha un programari antic i no compatible al dispositiu Bond - actualitza'l abans de continuar", "unknown": "Error inesperat" }, "flow_title": "Bond: {bond_id} ({host})", @@ -14,7 +15,7 @@ "data": { "access_token": "Token d'acc\u00e9s" }, - "description": "Voleu configurar {bond_id}?" + "description": "Vols configurar {bond_id}?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/en.json b/homeassistant/components/bond/translations/en.json index f141a450652..945b09b8186 100644 --- a/homeassistant/components/bond/translations/en.json +++ b/homeassistant/components/bond/translations/en.json @@ -25,4 +25,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/es.json b/homeassistant/components/bond/translations/es.json index 063915421e3..d9918238515 100644 --- a/homeassistant/components/bond/translations/es.json +++ b/homeassistant/components/bond/translations/es.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "old_firmware": "Firmware antiguo no compatible en el dispositivo Bond - actual\u00edzalo antes de continuar", "unknown": "Error inesperado" }, "flow_title": "Bond: {bond_id} ({host})", diff --git a/homeassistant/components/bond/translations/it.json b/homeassistant/components/bond/translations/it.json index 5b1434e9b63..d3ac1ab6b49 100644 --- a/homeassistant/components/bond/translations/it.json +++ b/homeassistant/components/bond/translations/it.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", + "old_firmware": "Firmware precedente non supportato sul dispositivo Bond - si prega di aggiornare prima di continuare", "unknown": "Errore imprevisto" }, "flow_title": "Bond: {bond_id} ({host})", diff --git a/homeassistant/components/bond/translations/lb.json b/homeassistant/components/bond/translations/lb.json index abce9d5efb9..d3ea1681751 100644 --- a/homeassistant/components/bond/translations/lb.json +++ b/homeassistant/components/bond/translations/lb.json @@ -1,11 +1,22 @@ { "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, "error": { "cannot_connect": "Feeler beim verbannen", "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "old_firmware": "Net \u00ebnnerst\u00ebtzten al Firmware op dem Bond Apparat - aktualis\u00e9ier iers Du weider fiers", "unknown": "Onerwaarte Feeler" }, + "flow_title": "Bond: {bond_id} ({host})", "step": { + "confirm": { + "data": { + "access_token": "Acc\u00e8s jeton" + }, + "description": "Soll {bond_id} konfigur\u00e9iert ginn?" + }, "user": { "data": { "access_token": "Acc\u00e8s jeton", diff --git a/homeassistant/components/bond/translations/no.json b/homeassistant/components/bond/translations/no.json index 1a1c8792dc0..5629512a44c 100644 --- a/homeassistant/components/bond/translations/no.json +++ b/homeassistant/components/bond/translations/no.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes.", "invalid_auth": "Ugyldig godkjenning", + "old_firmware": "Gammel fastvare som ikke st\u00f8ttes p\u00e5 Bond-enheten \u2013 vennligst oppgrader f\u00f8r du fortsetter", "unknown": "Uventet feil" }, "flow_title": "Obligasjon: {bond_id} ( {host} )", diff --git a/homeassistant/components/bond/translations/pt-BR.json b/homeassistant/components/bond/translations/pt-BR.json new file mode 100644 index 00000000000..a58a0045e46 --- /dev/null +++ b/homeassistant/components/bond/translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "Bond: {bond_id} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/ru.json b/homeassistant/components/bond/translations/ru.json index 566b04e1af8..493b8e141ce 100644 --- a/homeassistant/components/bond/translations/ru.json +++ b/homeassistant/components/bond/translations/ru.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "old_firmware": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430 \u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "Bond {bond_id} ({host})", diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json index 915ff9b6a05..cbb42aee925 100644 --- a/homeassistant/components/bond/translations/zh-Hant.json +++ b/homeassistant/components/bond/translations/zh-Hant.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "old_firmware": "Bond \u8a2d\u5099\u4f7f\u7528\u4e0d\u652f\u63f4\u7684\u820a\u7248\u672c\u97cc\u9ad4 - \u8acb\u66f4\u65b0\u5f8c\u518d\u7e7c\u7e8c", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "Bond\uff1a{bond_id} ({host})", diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index cd687d8f2d0..ad5907b4678 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -21,8 +21,7 @@ "data": { "host": "Vert" }, - "description": "Sett opp Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: [https://www.home-assistant.io/integrations/braviatv](https://www.home-assistant.io/integrations/braviatv)\n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5.", - "title": "" + "description": "Sett opp Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: [https://www.home-assistant.io/integrations/braviatv](https://www.home-assistant.io/integrations/braviatv)\n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5." } } }, diff --git a/homeassistant/components/broadlink/translations/ca.json b/homeassistant/components/broadlink/translations/ca.json new file mode 100644 index 00000000000..3e642b0f6b5 --- /dev/null +++ b/homeassistant/components/broadlink/translations/ca.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "Ja hi ha un flux de configuraci\u00f3 en curs per a aquest dispositiu", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids", + "unknown": "Error inesperat" + }, + "flow_title": "{name} ({model} a {host})", + "step": { + "auth": { + "title": "Autentica't al dispositiu" + }, + "finish": { + "data": { + "name": "Nom" + }, + "title": "Tria un nom per al dispositiu" + }, + "reset": { + "description": "El dispositiu est\u00e0 bloquejat per autenticaci\u00f3. Segueix les instruccions per desbloquejar-lo:\n 1. Restableix de f\u00e0brica el dispositiu. \n 2. Utilitza l'aplicaci\u00f3 oficial per afegir el dispositiu a la teva xarxa local. \n 3. Atura'l. No acabis la configuraci\u00f3. Tanca l'aplicaci\u00f3. \n 4. Fes clic a Envia.", + "title": "Desbloqueig de dispositiu" + }, + "unlock": { + "data": { + "unlock": "S\u00ed, fes-ho." + }, + "description": "El dispositiu est\u00e0 bloquejat. Aix\u00f2 pot donar problemes d'autenticaci\u00f3 a Home Assistant. Vols desbloquejar-lo?", + "title": "Desbloqueig de dispositiu (opcional)" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "timeout": "Temps m\u00e0xim d'espera" + }, + "title": "Connexi\u00f3 amb el dispositiu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/en.json b/homeassistant/components/broadlink/translations/en.json index 3784ef7eb89..fa3feb88008 100644 --- a/homeassistant/components/broadlink/translations/en.json +++ b/homeassistant/components/broadlink/translations/en.json @@ -1,46 +1,46 @@ { - "config": { - "flow_title": "{name} ({model} at {host})", - "step": { - "user": { - "title": "Connect to the device", - "data": { - "host": "Host", - "timeout": "Timeout" + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "There is already a configuration flow in progress for this device", + "cannot_connect": "Failed to connect", + "invalid_host": "Invalid hostname or IP address", + "unknown": "Unexpected error" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_host": "Invalid hostname or IP address", + "unknown": "Unexpected error" + }, + "flow_title": "{name} ({model} at {host})", + "step": { + "auth": { + "title": "Authenticate to the device" + }, + "finish": { + "data": { + "name": "Name" + }, + "title": "Choose a name for the device" + }, + "reset": { + "description": "Your device is locked for authentication. Follow the instructions to unlock it:\n1. Factory reset the device.\n2. Use the official app to add the device to your local network.\n3. Stop. Do not finish the setup. Close the app.\n4. Click Submit.", + "title": "Unlock the device" + }, + "unlock": { + "data": { + "unlock": "Yes, do it." + }, + "description": "Your device is locked. This can lead to authentication problems in Home Assistant. Would you like to unlock it?", + "title": "Unlock the device (optional)" + }, + "user": { + "data": { + "host": "Host", + "timeout": "Timeout" + }, + "title": "Connect to the device" + } } - }, - "auth": { - "title": "Authenticate to the device" - }, - "reset": { - "title": "Unlock the device", - "description": "Your device is locked for authentication. Follow the instructions to unlock it:\n1. Factory reset the device.\n2. Use the official app to add the device to your local network.\n3. Stop. Do not finish the setup. Close the app.\n4. Click Submit." - }, - "unlock": { - "title": "Unlock the device (optional)", - "description": "Your device is locked. This can lead to authentication problems in Home Assistant. Would you like to unlock it?", - "data": { - "unlock": "Yes, do it." - } - }, - "finish": { - "title": "Choose a name for the device", - "data": { - "name": "Name" - } - } - }, - "abort": { - "already_configured": "Device is already configured", - "already_in_progress": "There is already a configuration flow in progress for this device", - "cannot_connect": "Failed to connect", - "invalid_host": "Invalid hostname or IP address", - "unknown": "Unexpected error" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_host": "Invalid hostname or IP address", - "unknown": "Unexpected error" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json new file mode 100644 index 00000000000..fdceeabd2cd --- /dev/null +++ b/homeassistant/components/broadlink/translations/es.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "Ya hay un flujo de configuraci\u00f3n en curso para este dispositivo", + "cannot_connect": "No se pudo conectar", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", + "unknown": "Error inesperado" + }, + "flow_title": "{name} ( {model} en {host} )", + "step": { + "auth": { + "title": "Autenticarse en el dispositivo" + }, + "finish": { + "data": { + "name": "Nombre" + }, + "title": "Elija un nombre para el dispositivo" + }, + "reset": { + "description": "Su dispositivo est\u00e1 bloqueado para la autenticaci\u00f3n. Siga las instrucciones para desbloquearlo:\n1. Reinicie el dispositivo de f\u00e1brica.\n2. Use la aplicaci\u00f3n oficial para agregar el dispositivo a su red local.\n3. Pare. No termine la configuraci\u00f3n. Cierre la aplicaci\u00f3n.\n4. Haga clic en Enviar.", + "title": "Desbloquear el dispositivo" + }, + "unlock": { + "data": { + "unlock": "S\u00ed, hazlo." + }, + "description": "Tu dispositivo est\u00e1 bloqueado. Esto puede provocar problemas de autenticaci\u00f3n en Home Assistant. \u00bfQuieres desbloquearlo?", + "title": "Desbloquear el dispositivo (opcional)" + }, + "user": { + "data": { + "host": "Host", + "timeout": "Se acab\u00f3 el tiempo" + }, + "title": "Conectarse al dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/fr.json b/homeassistant/components/broadlink/translations/fr.json new file mode 100644 index 00000000000..c3fd625352b --- /dev/null +++ b/homeassistant/components/broadlink/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_in_progress": "Il y a d\u00e9j\u00e0 un processus de configuration en cours pour cet appareil", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + }, + "error": { + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + }, + "step": { + "auth": { + "title": "S'authentifier sur l'appareil" + }, + "finish": { + "data": { + "name": "Nom" + }, + "title": "Choisissez un nom pour l'appareil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/it.json b/homeassistant/components/broadlink/translations/it.json new file mode 100644 index 00000000000..939925104fd --- /dev/null +++ b/homeassistant/components/broadlink/translations/it.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "\u00c8 gi\u00e0 in corso un flusso di configurazione per questo dispositivo", + "cannot_connect": "Impossibile connettersi", + "invalid_host": "Nome host o indirizzo IP non valido", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_host": "Nome host o indirizzo IP non valido", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name} ({model} presso {host})", + "step": { + "auth": { + "title": "Eseguire l'autenticazione al dispositivo" + }, + "finish": { + "data": { + "name": "Nome" + }, + "title": "Scegliere un nome per il dispositivo" + }, + "reset": { + "description": "Il tuo dispositivo \u00e8 bloccato per l'autenticazione. Segui le istruzioni per sbloccarlo: \n 1. Ripristinare le impostazioni di fabbrica del dispositivo. \n 2. Usa l'app ufficiale per aggiungere il dispositivo alla tua rete locale. \n 3. Fermati. Non finire la configurazione. Chiudi l'app. \n 4. Fare clic su Invia.", + "title": "Sbloccare il dispositivo" + }, + "unlock": { + "data": { + "unlock": "S\u00ec, fallo." + }, + "description": "Il tuo dispositivo \u00e8 bloccato. Ci\u00f2 pu\u00f2 causare problemi di autenticazione in Home Assistant. Vorresti sbloccarlo?", + "title": "Sblocca il dispositivo (opzionale)" + }, + "user": { + "data": { + "host": "Host", + "timeout": "Tempo scaduto" + }, + "title": "Connettiti al dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/lb.json b/homeassistant/components/broadlink/translations/lb.json new file mode 100644 index 00000000000..0872c01b608 --- /dev/null +++ b/homeassistant/components/broadlink/translations/lb.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Et ass schonn ee Konfiguratioun's Oflaf fir d\u00ebsen Apparat am gaang.", + "cannot_connect": "Feeler beim verbannen", + "invalid_host": "Ong\u00ebltege Numm oder IP Adresse.", + "unknown": "Onerwaarte Feeler" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_host": "Ong\u00ebltege Numm oder IP Adresse.", + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "{name} ({model} um {host})", + "step": { + "auth": { + "title": "Mam Apparat verbannen" + }, + "finish": { + "data": { + "name": "Numm" + }, + "title": "Numm auswielen fir den Apparat" + }, + "reset": { + "title": "Apparat entsp\u00e4ren" + }, + "unlock": { + "data": { + "unlock": "Jo, mach \u00ebt" + }, + "description": "D\u00e4in Apparat ass gespaart. D\u00ebs kann zu Authentifikatiouns Problemer am Home Assistant f\u00e9ieren. W\u00eblls du et enstp\u00e4ren?", + "title": "Apparat entsp\u00e4ren (optionell)" + }, + "user": { + "data": { + "host": "Host", + "timeout": "Z\u00e4itiwwerscheidung" + }, + "title": "Mam Apparat verbannen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/no.json b/homeassistant/components/broadlink/translations/no.json new file mode 100644 index 00000000000..beeae80745d --- /dev/null +++ b/homeassistant/components/broadlink/translations/no.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Det p\u00e5g\u00e5r allerede en konfigurasjonsflyt for denne enheten", + "cannot_connect": "Tilkobling mislyktes.", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes.", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse", + "unknown": "Uventet feil" + }, + "flow_title": "{name} ( {model} hos {host} )", + "step": { + "auth": { + "title": "Autentiser til enheten" + }, + "finish": { + "data": { + "name": "Navn" + }, + "title": "Velg et navn p\u00e5 enheten" + }, + "reset": { + "description": "Enheten er l\u00e5st for godkjenning. F\u00f8lg instruksjonene for \u00e5 l\u00e5se den opp:\n1. Tilbakestill enheten til fabrikkstandard.\n2. Bruk den offisielle appen til \u00e5 legge til enheten i ditt lokale nettverk.\n3. Stopp. Ikke fullf\u00f8r oppsettet. Lukk appen.\n4. Klikk p\u00e5 Send.", + "title": "L\u00e5s opp enheten" + }, + "unlock": { + "data": { + "unlock": "Ja, gj\u00f8r det." + }, + "description": "Enheten er l\u00e5st. Dette kan f\u00f8re til godkjenningsproblemer i Home Assistant. Vil du l\u00e5se den opp?", + "title": "L\u00e5s opp enheten (valgfritt)" + }, + "user": { + "data": { + "host": "Vert", + "timeout": "Tidsavbrudd" + }, + "title": "Koble til enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/ru.json b/homeassistant/components/broadlink/translations/ru.json new file mode 100644 index 00000000000..f7d0cfab3d1 --- /dev/null +++ b/homeassistant/components/broadlink/translations/ru.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name} ({model}, {host})", + "step": { + "auth": { + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435" + }, + "finish": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "reset": { + "description": "\u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0434\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e: \n 1. \u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0431\u0440\u043e\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0430 \u0437\u0430\u0432\u043e\u0434\u0441\u043a\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n 2. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0434\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0412\u0430\u0448\u0443 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0443\u044e \u0441\u0435\u0442\u044c. \n 3. \u041d\u0435 \u0437\u0430\u043a\u0430\u043d\u0447\u0438\u0432\u0430\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0438 \u0437\u0430\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \n 4. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c.", + "title": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "unlock": { + "data": { + "unlock": "\u0414\u0430, \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e." + }, + "description": "\u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u043c \u0441 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0435\u0439 \u0432 Home Assistant. \u0425\u043e\u0442\u0438\u0442\u0435 \u0435\u0433\u043e \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c?", + "title": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/zh-Hant.json b/homeassistant/components/broadlink/translations/zh-Hant.json new file mode 100644 index 00000000000..741e8beb2f7 --- /dev/null +++ b/homeassistant/components/broadlink/translations/zh-Hant.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u6b64\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}\uff08\u4f4d\u65bc {host} \u4e4b {model} \uff09", + "step": { + "auth": { + "title": "\u8a8d\u8b49\u8a2d\u5099" + }, + "finish": { + "data": { + "name": "\u540d\u7a31" + }, + "title": "\u9078\u64c7\u8a2d\u5099\u540d\u7a31" + }, + "reset": { + "description": "\u8a2d\u5099\u5df2\u9396\u5b9a\u9700\u9032\u884c\u8a8d\u8b49\u3001\u8acb\u8ddf\u96a8\u6307\u793a\u9032\u884c\u89e3\u9396\uff1a\n1. \u91cd\u7f6e\u8a2d\u5099\u3002\n2. \u4f7f\u7528\u5b98\u65b9 App \u65b0\u589e\u8a2d\u5099\u81f3\u5340\u57df\u7db2\u8def\u3002\n3. \u4e2d\u65b7\uff0c\u4e0d\u8981\u5b8c\u6210\u8a2d\u5b9a\u3002\u95dc\u9589 App\u3002\n4. \u9ede\u9078\u50b3\u9001\u3002", + "title": "\u89e3\u9396\u8a2d\u5099" + }, + "unlock": { + "data": { + "unlock": "\u662f\uff0c\u57f7\u884c\u3002" + }, + "description": "\u8a2d\u5099\u5df2\u9396\u5b9a\uff0c\u53ef\u80fd\u5c0e\u81f4 Home Assistant \u8a8d\u8b49\u554f\u984c\uff0c\u662f\u5426\u8981\u89e3\u9396\uff1f", + "title": "\u89e3\u9396\u8a2d\u5099\uff08\u9078\u9805\uff09" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "timeout": "\u903e\u6642" + }, + "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index 8a9f811cddb..a1d35a44984 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -22,7 +22,7 @@ "data": { "type": "\u5370\u8868\u6a5f\u985e\u578b" }, - "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f {serial_number} \u4e4bBrother \u5370\u8868\u6a5f {model} \u65b0\u589e\u81f3 Home Assistant\uff1f", + "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Brother \u5370\u8868\u6a5f {model} \u65b0\u589e\u81f3 Home Assistant\uff1f", "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Brother \u5370\u8868\u6a5f" } } diff --git a/homeassistant/components/bsblan/translations/no.json b/homeassistant/components/bsblan/translations/no.json index 040349997f4..319fbb771af 100644 --- a/homeassistant/components/bsblan/translations/no.json +++ b/homeassistant/components/bsblan/translations/no.json @@ -11,8 +11,7 @@ "user": { "data": { "host": "Vert", - "passkey": "Tilgangsn\u00f8kkel streng", - "port": "" + "passkey": "Tilgangsn\u00f8kkel streng" }, "description": "Konfigurer din BSB-Lan-enhet for \u00e5 integrere med Home Assistant.", "title": "Koble til BSB-Lan-enheten" diff --git a/homeassistant/components/cert_expiry/translations/no.json b/homeassistant/components/cert_expiry/translations/no.json index a7aa3d1ab13..5832e0a0dd5 100644 --- a/homeassistant/components/cert_expiry/translations/no.json +++ b/homeassistant/components/cert_expiry/translations/no.json @@ -13,8 +13,7 @@ "user": { "data": { "host": "Vert", - "name": "Sertifikatets navn", - "port": "" + "name": "Sertifikatets navn" }, "title": "Definer sertifikatet som skal testes" } diff --git a/homeassistant/components/climate/translations/no.json b/homeassistant/components/climate/translations/no.json index 4ac58d07bbb..3117378191d 100644 --- a/homeassistant/components/climate/translations/no.json +++ b/homeassistant/components/climate/translations/no.json @@ -16,7 +16,6 @@ }, "state": { "_": { - "auto": "", "cool": "Kj\u00f8le", "dry": "T\u00f8rr", "fan_only": "Kun vifte", diff --git a/homeassistant/components/control4/translations/pt-BR.json b/homeassistant/components/control4/translations/pt-BR.json new file mode 100644 index 00000000000..931024c0e96 --- /dev/null +++ b/homeassistant/components/control4/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na conex\u00e3o" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/ru.json b/homeassistant/components/control4/translations/ru.json index 315fbb7b3f3..4f51641992b 100644 --- a/homeassistant/components/control4/translations/ru.json +++ b/homeassistant/components/control4/translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u043c\u0438 (\u0441\u0435\u043a.)" + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" } } } diff --git a/homeassistant/components/cover/translations/fr.json b/homeassistant/components/cover/translations/fr.json index 8c68faab5e0..31cb82a6e7b 100644 --- a/homeassistant/components/cover/translations/fr.json +++ b/homeassistant/components/cover/translations/fr.json @@ -19,9 +19,9 @@ "closed": "{entity_name} ferm\u00e9", "closing": "{entity_name} fermeture", "opened": "{entity_name} ouvert", - "opening": "{entity_name} ouverture", - "position": "{entity_name} changement de position", - "tilt_position": "{entity_name} changement d'inclinaison" + "opening": "Ouverture de {entity_name}", + "position": "Changement de position de {entity_name}", + "tilt_position": "Changement d'inclinaison de {entity_name}" } }, "state": { diff --git a/homeassistant/components/cover/translations/lb.json b/homeassistant/components/cover/translations/lb.json index 4aff8a3f329..959bfab863b 100644 --- a/homeassistant/components/cover/translations/lb.json +++ b/homeassistant/components/cover/translations/lb.json @@ -6,7 +6,8 @@ "open": "{entity_name} opmaachen", "open_tilt": "{entity_name} op Kipp stelle", "set_position": "{entity_name} positioun programm\u00e9ieren", - "set_tilt_position": "{entity_name} kipp positioun programm\u00e9ieren" + "set_tilt_position": "{entity_name} kipp positioun programm\u00e9ieren", + "stop": "Stop {entity_name}" }, "condition_type": { "is_closed": "{entity_name} ass zou", diff --git a/homeassistant/components/daikin/translations/ca.json b/homeassistant/components/daikin/translations/ca.json index 724cdac5f56..8233287e0d7 100644 --- a/homeassistant/components/daikin/translations/ca.json +++ b/homeassistant/components/daikin/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3" }, "error": { "device_fail": "Error inesperat", diff --git a/homeassistant/components/daikin/translations/en.json b/homeassistant/components/daikin/translations/en.json index cf0f679c7ca..83ac7913f67 100644 --- a/homeassistant/components/daikin/translations/en.json +++ b/homeassistant/components/daikin/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect" }, "error": { "device_fail": "Unexpected error", diff --git a/homeassistant/components/daikin/translations/es.json b/homeassistant/components/daikin/translations/es.json index 42b58e38438..9c3b81a3f51 100644 --- a/homeassistant/components/daikin/translations/es.json +++ b/homeassistant/components/daikin/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar" }, "error": { "device_fail": "Error inesperado", diff --git a/homeassistant/components/daikin/translations/it.json b/homeassistant/components/daikin/translations/it.json index 373dbcd53d2..07aa45e7d96 100644 --- a/homeassistant/components/daikin/translations/it.json +++ b/homeassistant/components/daikin/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi" }, "error": { "device_fail": "Errore imprevisto", diff --git a/homeassistant/components/daikin/translations/ko.json b/homeassistant/components/daikin/translations/ko.json index ff79df2de84..e5981dcb626 100644 --- a/homeassistant/components/daikin/translations/ko.json +++ b/homeassistant/components/daikin/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "device_fail": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/daikin/translations/lb.json b/homeassistant/components/daikin/translations/lb.json index 97c9d3a19dc..aa59b6dfa50 100644 --- a/homeassistant/components/daikin/translations/lb.json +++ b/homeassistant/components/daikin/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert" + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "cannot_connect": "Feeler beim verbannen" }, "error": { "device_fail": "Onerwaarte Feeler", diff --git a/homeassistant/components/daikin/translations/no.json b/homeassistant/components/daikin/translations/no.json index cb2f8cde40b..c06688a7e16 100644 --- a/homeassistant/components/daikin/translations/no.json +++ b/homeassistant/components/daikin/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes." }, "error": { "device_fail": "Uventet feil", diff --git a/homeassistant/components/daikin/translations/pt-BR.json b/homeassistant/components/daikin/translations/pt-BR.json index 7853563a53c..a844969172f 100644 --- a/homeassistant/components/daikin/translations/pt-BR.json +++ b/homeassistant/components/daikin/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na conex\u00e3o" }, "error": { "device_fail": "Erro inesperado", diff --git a/homeassistant/components/daikin/translations/ru.json b/homeassistant/components/daikin/translations/ru.json index 5332ac046e1..32043f257d1 100644 --- a/homeassistant/components/daikin/translations/ru.json +++ b/homeassistant/components/daikin/translations/ru.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "error": { "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", - "device_timeout": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "device_timeout": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "forbidden": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." }, "step": { diff --git a/homeassistant/components/daikin/translations/zh-Hans.json b/homeassistant/components/daikin/translations/zh-Hans.json index d27301c2f20..307fd15324a 100644 --- a/homeassistant/components/daikin/translations/zh-Hans.json +++ b/homeassistant/components/daikin/translations/zh-Hans.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8bbe\u5907\u5df2\u914d\u7f6e\u5b8c\u6210" + "already_configured": "\u8bbe\u5907\u5df2\u914d\u7f6e\u5b8c\u6210", + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index 20121842a37..60ffcb8aba3 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { "device_fail": "\u672a\u9810\u671f\u932f\u8aa4", diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 231901c4cd4..6eb5624f05f 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -23,8 +23,7 @@ }, "manual_input": { "data": { - "host": "Vert", - "port": "" + "host": "Vert" } }, "user": { @@ -48,12 +47,6 @@ "left": "Venstre", "open": "\u00c5pen", "right": "H\u00f8yre", - "side_1": "", - "side_2": "", - "side_3": "", - "side_4": "", - "side_5": "", - "side_6": "", "top_buttons": "\u00d8verste knappene", "turn_off": "Skru av", "turn_on": "Sl\u00e5 p\u00e5" diff --git a/homeassistant/components/deconz/translations/pt-BR.json b/homeassistant/components/deconz/translations/pt-BR.json index e8f1f2e39e2..dccbb0f69ca 100644 --- a/homeassistant/components/deconz/translations/pt-BR.json +++ b/homeassistant/components/deconz/translations/pt-BR.json @@ -19,6 +19,11 @@ "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", "title": "Linkar com deCONZ" + }, + "manual_input": { + "data": { + "port": "Porta" + } } } }, diff --git a/homeassistant/components/demo/translations/no.json b/homeassistant/components/demo/translations/no.json index 5003b9da568..3e7dfafac08 100644 --- a/homeassistant/components/demo/translations/no.json +++ b/homeassistant/components/demo/translations/no.json @@ -16,6 +16,5 @@ } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/ru.json b/homeassistant/components/denonavr/translations/ru.json index 2470e19f121..79e8c213e5f 100644 --- a/homeassistant/components/denonavr/translations/ru.json +++ b/homeassistant/components/denonavr/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437. \u0415\u0441\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u0430\u0431\u0435\u043b\u044c Ethernet \u0438 \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u043a\u0430\u0431\u0435\u043b\u044c.", "not_denonavr_manufacturer": "\u042d\u0442\u043e \u043d\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon. \u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442.", diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json index 19c8d2653c1..08b70ba7a4e 100644 --- a/homeassistant/components/devolo_home_control/translations/no.json +++ b/homeassistant/components/devolo_home_control/translations/no.json @@ -13,10 +13,8 @@ "mydevolo_url": "mydevolo URL", "password": "Passord", "username": "E-postadresse / devolo-ID" - }, - "title": "" + } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/et.json b/homeassistant/components/devolo_home_control/translations/pt-BR.json similarity index 64% rename from homeassistant/components/tradfri/translations/et.json rename to homeassistant/components/devolo_home_control/translations/pt-BR.json index 5d0a728407a..4a9930dd95d 100644 --- a/homeassistant/components/tradfri/translations/et.json +++ b/homeassistant/components/devolo_home_control/translations/pt-BR.json @@ -1,9 +1,9 @@ { "config": { "step": { - "auth": { + "user": { "data": { - "host": "" + "password": "Senha" } } } diff --git a/homeassistant/components/dexcom/translations/no.json b/homeassistant/components/dexcom/translations/no.json index 61ad015b5a4..de99cfe0fbc 100644 --- a/homeassistant/components/dexcom/translations/no.json +++ b/homeassistant/components/dexcom/translations/no.json @@ -12,7 +12,6 @@ "user": { "data": { "password": "Passord", - "server": "", "username": "Brukernavn" }, "description": "Angi Dexcom Share-legitimasjon", diff --git a/homeassistant/components/dexcom/translations/ru.json b/homeassistant/components/dexcom/translations/ru.json index 270abf370c2..85e381be8c8 100644 --- a/homeassistant/components/dexcom/translations/ru.json +++ b/homeassistant/components/dexcom/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "account_error": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", - "session_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "session_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/directv/translations/no.json b/homeassistant/components/directv/translations/no.json index c6db33d32d0..88f36decaea 100644 --- a/homeassistant/components/directv/translations/no.json +++ b/homeassistant/components/directv/translations/no.json @@ -7,7 +7,6 @@ "error": { "cannot_connect": "Tilkobling feilet" }, - "flow_title": "", "step": { "ssdp_confirm": { "description": "Vil du sette opp {name} ?" diff --git a/homeassistant/components/directv/translations/ru.json b/homeassistant/components/directv/translations/ru.json index e2625644238..a3a340e5ce5 100644 --- a/homeassistant/components/directv/translations/ru.json +++ b/homeassistant/components/directv/translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/doorbird/translations/ru.json b/homeassistant/components/doorbird/translations/ru.json index bfe0968c847..9ca74bd7780 100644 --- a/homeassistant/components/doorbird/translations/ru.json +++ b/homeassistant/components/doorbird/translations/ru.json @@ -6,7 +6,7 @@ "not_doorbird_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 DoorBird." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/dunehd/translations/no.json b/homeassistant/components/dunehd/translations/no.json index e395c28b7a9..6230845a09e 100644 --- a/homeassistant/components/dunehd/translations/no.json +++ b/homeassistant/components/dunehd/translations/no.json @@ -13,8 +13,7 @@ "data": { "host": "Vert" }, - "description": "Konfigurer Dune HD-integrering. Hvis du har problemer med konfigurasjonen, kan du g\u00e5 til: https://www.home-assistant.io/integrations/dunehd \n\nKontroller at spilleren er sl\u00e5tt p\u00e5.", - "title": "" + "description": "Konfigurer Dune HD-integrering. Hvis du har problemer med konfigurasjonen, kan du g\u00e5 til: https://www.home-assistant.io/integrations/dunehd \n\nKontroller at spilleren er sl\u00e5tt p\u00e5." } } } diff --git a/homeassistant/components/dunehd/translations/ru.json b/homeassistant/components/dunehd/translations/ru.json index 04f2dc6dd0f..be35fe8b092 100644 --- a/homeassistant/components/dunehd/translations/ru.json +++ b/homeassistant/components/dunehd/translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "already_configured": "\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.", - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "step": { diff --git a/homeassistant/components/eafm/translations/ca.json b/homeassistant/components/eafm/translations/ca.json new file mode 100644 index 00000000000..317f9ba6f56 --- /dev/null +++ b/homeassistant/components/eafm/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_stations": "No s'han trobat estacions de control d'inundacions." + }, + "step": { + "user": { + "data": { + "station": "Estaci\u00f3" + }, + "description": "Selecciona l'estaci\u00f3 a monitoritzar", + "title": "Seguiment d'estaci\u00f3 de control d'inundacions" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/en.json b/homeassistant/components/eafm/translations/en.json index 3a3ddae390b..19024d80a5e 100644 --- a/homeassistant/components/eafm/translations/en.json +++ b/homeassistant/components/eafm/translations/en.json @@ -1,17 +1,17 @@ { "config": { + "abort": { + "already_configured": "Device is already configured", + "no_stations": "No flood monitoring stations found." + }, "step": { "user": { - "title": "Track a flood monitoring station", - "description": "Select the station you want to monitor", "data": { "station": "Station" - } + }, + "description": "Select the station you want to monitor", + "title": "Track a flood monitoring station" } - }, - "abort": { - "no_stations": "No flood monitoring stations found.", - "already_configured": "This station is already configured." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/es.json b/homeassistant/components/eafm/translations/es.json new file mode 100644 index 00000000000..01dca9b3af3 --- /dev/null +++ b/homeassistant/components/eafm/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "no_stations": "No se encontraron estaciones de monitoreo de inundaciones." + }, + "step": { + "user": { + "data": { + "station": "Estaci\u00f3n" + }, + "description": "Seleccione la estaci\u00f3n que desea monitorear", + "title": "Rastrear una estaci\u00f3n de monitoreo de inundaciones" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/it.json b/homeassistant/components/eafm/translations/it.json new file mode 100644 index 00000000000..9ff389df480 --- /dev/null +++ b/homeassistant/components/eafm/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "no_stations": "Non sono state trovate stazioni di monitoraggio di allagamento." + }, + "step": { + "user": { + "data": { + "station": "Stazione" + }, + "description": "Seleziona la stazione che desideri monitorare", + "title": "Traccia una stazione di monitoraggio delle inondazioni" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/lb.json b/homeassistant/components/eafm/translations/lb.json new file mode 100644 index 00000000000..78996aa1bfd --- /dev/null +++ b/homeassistant/components/eafm/translations/lb.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "no_stations": "Keng H\u00e9ichwaasser Iwwerwaachungs Statioun fonnt." + }, + "step": { + "user": { + "data": { + "station": "Statioun" + }, + "description": "Statioun auswielen d\u00e9i iwwerwaacht soll ginn", + "title": "H\u00e9ichwaasser Iwwerwaachungs Statioun suiv\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/no.json b/homeassistant/components/eafm/translations/no.json new file mode 100644 index 00000000000..c29a206497c --- /dev/null +++ b/homeassistant/components/eafm/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "no_stations": "Ingen flomoverv\u00e5kningsstasjoner funnet." + }, + "step": { + "user": { + "data": { + "station": "Stasjon" + }, + "description": "Velg stasjonen du vil overv\u00e5ke", + "title": "Spor en flomoverv\u00e5kningsstasjon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/ru.json b/homeassistant/components/eafm/translations/ru.json new file mode 100644 index 00000000000..b101b2255b8 --- /dev/null +++ b/homeassistant/components/eafm/translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "no_stations": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438 \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u043d\u0430\u0432\u043e\u0434\u043d\u0435\u043d\u0438\u0439 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." + }, + "step": { + "user": { + "data": { + "station": "\u0421\u0442\u0430\u043d\u0446\u0438\u044f" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u043d\u0446\u0438\u044e \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430", + "title": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438 \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u043d\u0430\u0432\u043e\u0434\u043d\u0435\u043d\u0438\u0439" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/zh-Hant.json b/homeassistant/components/eafm/translations/zh-Hant.json new file mode 100644 index 00000000000..5da4b6d7c09 --- /dev/null +++ b/homeassistant/components/eafm/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_stations": "\u627e\u4e0d\u5230\u7b26\u5408\u7684\u76e3\u63a7\u7ad9\u3002" + }, + "step": { + "user": { + "data": { + "station": "\u76e3\u63a7\u7ad9" + }, + "description": "\u9078\u64c7\u6240\u8981\u76e3\u63a7\u7684\u7ad9\u9ede", + "title": "\u8ffd\u8e64\u6d2a\u6c34\u76e3\u63a7\u7ad9" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/no.json b/homeassistant/components/elgato/translations/no.json index bb7e56211de..9c78b3411e3 100644 --- a/homeassistant/components/elgato/translations/no.json +++ b/homeassistant/components/elgato/translations/no.json @@ -11,8 +11,7 @@ "step": { "user": { "data": { - "host": "Vert", - "port": "" + "host": "Vert" }, "description": "Sett opp Elgato Key Light for \u00e5 integrere med Home Assistant." }, diff --git a/homeassistant/components/elgato/translations/pt-BR.json b/homeassistant/components/elgato/translations/pt-BR.json index 1684ce82433..02edb707618 100644 --- a/homeassistant/components/elgato/translations/pt-BR.json +++ b/homeassistant/components/elgato/translations/pt-BR.json @@ -1,6 +1,11 @@ { "config": { "step": { + "user": { + "data": { + "port": "Porta" + } + }, "zeroconf_confirm": { "description": "Deseja adicionar o Elgato Key Light n\u00famero de s\u00e9rie ` {serial_number} ` ao Home Assistant?", "title": "Dispositivo Elgato Key Light descoberto" diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index 9952a3be6a3..596dc530a2f 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -17,7 +17,7 @@ "description": "\u8a2d\u5b9a Elgato Key \u7167\u660e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u5c07 Elgato Key \u7167\u660e\u5e8f\u865f `{serial_number}` \u65b0\u589e\u81f3 Home Assistant\uff1f", + "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Elgato Key \u7167\u660e\u8a2d\u5099\u65b0\u589e\u81f3 Home Assistant\uff1f", "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato Key \u7167\u660e\u8a2d\u5099" } } diff --git a/homeassistant/components/tibber/translations/pt.json b/homeassistant/components/elkm1/translations/pt-BR.json similarity index 50% rename from homeassistant/components/tibber/translations/pt.json rename to homeassistant/components/elkm1/translations/pt-BR.json index 243987422dd..932b4b8a72e 100644 --- a/homeassistant/components/tibber/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt-BR.json @@ -3,11 +3,9 @@ "step": { "user": { "data": { - "access_token": "" - }, - "title": "" + "username": "Usu\u00e1rio" + } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/et.json b/homeassistant/components/emulated_roku/translations/et.json index b94548b44af..d6a9fded4b6 100644 --- a/homeassistant/components/emulated_roku/translations/et.json +++ b/homeassistant/components/emulated_roku/translations/et.json @@ -3,11 +3,9 @@ "step": { "user": { "data": { - "host_ip": "", "name": "Nimi" } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/no.json b/homeassistant/components/enocean/translations/no.json index a3fc35edcc8..b87607d2f35 100644 --- a/homeassistant/components/enocean/translations/no.json +++ b/homeassistant/components/enocean/translations/no.json @@ -22,6 +22,5 @@ "title": "Angi banen til din ENOcean dongle" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 3c2dafff34d..9a23a04b540 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -23,8 +23,7 @@ }, "user": { "data": { - "host": "Vert", - "port": "" + "host": "Vert" }, "description": "Vennligst fyll inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node." } diff --git a/homeassistant/components/flick_electric/translations/pt-BR.json b/homeassistant/components/flick_electric/translations/pt-BR.json index 2f05df0cb4d..5d7f10c1e03 100644 --- a/homeassistant/components/flick_electric/translations/pt-BR.json +++ b/homeassistant/components/flick_electric/translations/pt-BR.json @@ -12,7 +12,9 @@ "user": { "data": { "client_id": "ID do cliente (Opcional)", - "client_secret": "Segredo do cliente (Opcional)" + "client_secret": "Segredo do cliente (Opcional)", + "password": "Senha", + "username": "Usu\u00e1rio" }, "title": "Credenciais de login do Flick" } diff --git a/homeassistant/components/flo/translations/ca.json b/homeassistant/components/flo/translations/ca.json new file mode 100644 index 00000000000..9f2af24fb0c --- /dev/null +++ b/homeassistant/components/flo/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + }, + "title": "flo" +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/en.json b/homeassistant/components/flo/translations/en.json index 9434a81b3e6..2b5ee5b33ee 100644 --- a/homeassistant/components/flo/translations/en.json +++ b/homeassistant/components/flo/translations/en.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" }, "step": { "user": { "data": { - "host": "[%key:common::config_flow::data::host%]", - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" + "host": "Host", + "password": "Password", + "username": "Username" } } } diff --git a/homeassistant/components/flo/translations/es.json b/homeassistant/components/flo/translations/es.json new file mode 100644 index 00000000000..cf11e586483 --- /dev/null +++ b/homeassistant/components/flo/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Fallo al conectar", + "invalid_auth": "Autentificaci\u00f3n no valida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + } + } + }, + "title": "flo" +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/it.json b/homeassistant/components/flo/translations/it.json new file mode 100644 index 00000000000..824e021aa58 --- /dev/null +++ b/homeassistant/components/flo/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Nome utente" + } + } + } + }, + "title": "flo" +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/lb.json b/homeassistant/components/flo/translations/lb.json new file mode 100644 index 00000000000..33aaba2878f --- /dev/null +++ b/homeassistant/components/flo/translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwuert", + "username": "Benotzernumm" + } + } + } + }, + "title": "flo" +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/no.json b/homeassistant/components/flo/translations/no.json new file mode 100644 index 00000000000..d02310c9215 --- /dev/null +++ b/homeassistant/components/flo/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes.", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "username": "Brukernavn" + } + } + } + }, + "title": "Flo" +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/pt-BR.json b/homeassistant/components/flo/translations/pt-BR.json new file mode 100644 index 00000000000..026edf7221c --- /dev/null +++ b/homeassistant/components/flo/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/ru.json b/homeassistant/components/flo/translations/ru.json new file mode 100644 index 00000000000..5d55f2d523f --- /dev/null +++ b/homeassistant/components/flo/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + }, + "title": "flo" +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/zh-Hant.json b/homeassistant/components/flo/translations/zh-Hant.json new file mode 100644 index 00000000000..3856bfdec9a --- /dev/null +++ b/homeassistant/components/flo/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "title": "flo" +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/nl.json b/homeassistant/components/forked_daapd/translations/nl.json index 73dd47b2498..5391615fcb1 100644 --- a/homeassistant/components/forked_daapd/translations/nl.json +++ b/homeassistant/components/forked_daapd/translations/nl.json @@ -11,7 +11,7 @@ "wrong_password": "Onjuist wachtwoord.", "wrong_server_type": "De forked-daapd-integratie vereist een forked-daapd-server met versie >= 27.0." }, - "flow_title": "forked-daapd server: {naam} ({host})", + "flow_title": "forked-daapd server: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/no.json b/homeassistant/components/forked_daapd/translations/no.json index ac58ddf9639..d86d47c4137 100644 --- a/homeassistant/components/forked_daapd/translations/no.json +++ b/homeassistant/components/forked_daapd/translations/no.json @@ -17,8 +17,7 @@ "data": { "host": "Vert", "name": "Vennlig navn", - "password": "API-passord (la st\u00e5 tomt hvis ingen passord)", - "port": "" + "password": "API-passord (la st\u00e5 tomt hvis ingen passord)" }, "title": "Konfigurere forked-daapd-enhet" } diff --git a/homeassistant/components/freebox/translations/no.json b/homeassistant/components/freebox/translations/no.json index 0ec9bf70ecd..f93450837a2 100644 --- a/homeassistant/components/freebox/translations/no.json +++ b/homeassistant/components/freebox/translations/no.json @@ -15,10 +15,8 @@ }, "user": { "data": { - "host": "Vert", - "port": "" - }, - "title": "" + "host": "Vert" + } } } } diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index db7b202ac5f..3bd90697596 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -7,7 +7,7 @@ "not_supported": "Connect\u00e9 \u00e0 AVM FRITZ! Box mais impossible de contr\u00f4ler les appareils Smart Home." }, "error": { - "auth_failed": "Le nom d'utilisateur et / ou le mot de passe sont incorrects." + "auth_failed": "Le nom d'utilisateur et/ou le mot de passe sont incorrects." }, "flow_title": "AVM FRITZ!Box : {name}", "step": { diff --git a/homeassistant/components/fritzbox/translations/pt-BR.json b/homeassistant/components/fritzbox/translations/pt-BR.json index 6fd7f35d8c5..befa6942be9 100644 --- a/homeassistant/components/fritzbox/translations/pt-BR.json +++ b/homeassistant/components/fritzbox/translations/pt-BR.json @@ -5,7 +5,17 @@ }, "step": { "confirm": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, "description": "Voc\u00ea quer configurar o {name}?" + }, + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } } } } diff --git a/homeassistant/components/garmin_connect/translations/no.json b/homeassistant/components/garmin_connect/translations/no.json index 9058d46d02a..1c814306b3b 100644 --- a/homeassistant/components/garmin_connect/translations/no.json +++ b/homeassistant/components/garmin_connect/translations/no.json @@ -15,8 +15,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Fyll inn legitimasjonen din.", - "title": "" + "description": "Fyll inn legitimasjonen din." } } } diff --git a/homeassistant/components/gdacs/translations/no.json b/homeassistant/components/gdacs/translations/no.json index 372a24c0b38..3ca22c398e0 100644 --- a/homeassistant/components/gdacs/translations/no.json +++ b/homeassistant/components/gdacs/translations/no.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "radius": "" - }, "title": "Fyll ut filterdetaljene." } } diff --git a/homeassistant/components/geonetnz_quakes/translations/no.json b/homeassistant/components/geonetnz_quakes/translations/no.json index fc3b339d807..3ca22c398e0 100644 --- a/homeassistant/components/geonetnz_quakes/translations/no.json +++ b/homeassistant/components/geonetnz_quakes/translations/no.json @@ -5,10 +5,6 @@ }, "step": { "user": { - "data": { - "mmi": "", - "radius": "" - }, "title": "Fyll ut filterdetaljene." } } diff --git a/homeassistant/components/geonetnz_volcano/translations/no.json b/homeassistant/components/geonetnz_volcano/translations/no.json index 646afcc1d16..50ffa06071e 100644 --- a/homeassistant/components/geonetnz_volcano/translations/no.json +++ b/homeassistant/components/geonetnz_volcano/translations/no.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "radius": "" - }, "title": "Fyll inn dine filterdetaljer." } } diff --git a/homeassistant/components/gios/translations/no.json b/homeassistant/components/gios/translations/no.json index 784b75c9ee5..7df7ba57b3b 100644 --- a/homeassistant/components/gios/translations/no.json +++ b/homeassistant/components/gios/translations/no.json @@ -14,8 +14,7 @@ "name": "Navn p\u00e5 integrasjon", "station_id": "ID til m\u00e5lestasjon" }, - "description": "Sett opp GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) luftkvalitet integrasjon. Hvis du trenger hjelp med konfigurasjonen ta en titt her: https://www.home-assistant.io/integrations/gios", - "title": "" + "description": "Sett opp GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) luftkvalitet integrasjon. Hvis du trenger hjelp med konfigurasjonen ta en titt her: https://www.home-assistant.io/integrations/gios" } } } diff --git a/homeassistant/components/glances/translations/no.json b/homeassistant/components/glances/translations/no.json index dd593c4add6..666aaa6bf00 100644 --- a/homeassistant/components/glances/translations/no.json +++ b/homeassistant/components/glances/translations/no.json @@ -13,7 +13,6 @@ "host": "Vert", "name": "Navn", "password": "Passord", - "port": "", "ssl": "Bruk SSL / TLS for \u00e5 koble til Glances-systemet", "username": "Brukernavn", "verify_ssl": "Bekreft sertifiseringen av systemet", diff --git a/homeassistant/components/glances/translations/pt-BR.json b/homeassistant/components/glances/translations/pt-BR.json index 05ea657c8b3..7f5535fd04b 100644 --- a/homeassistant/components/glances/translations/pt-BR.json +++ b/homeassistant/components/glances/translations/pt-BR.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "username": "Nome de usu\u00e1rio" + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/gogogate2/translations/ru.json b/homeassistant/components/gogogate2/translations/ru.json index 9f428658820..f7f88db1d1b 100644 --- a/homeassistant/components/gogogate2/translations/ru.json +++ b/homeassistant/components/gogogate2/translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." }, "step": { diff --git a/homeassistant/components/group/translations/nb.json b/homeassistant/components/group/translations/nb.json index 14ac7fac24f..7d2edd69113 100644 --- a/homeassistant/components/group/translations/nb.json +++ b/homeassistant/components/group/translations/nb.json @@ -6,7 +6,6 @@ "locked": "L\u00e5st", "not_home": "Borte", "off": "Av", - "ok": "", "on": "P\u00e5", "open": "\u00c5pen", "problem": "Problem", diff --git a/homeassistant/components/group/translations/no.json b/homeassistant/components/group/translations/no.json index 763021190c1..698af4fe68c 100644 --- a/homeassistant/components/group/translations/no.json +++ b/homeassistant/components/group/translations/no.json @@ -6,10 +6,8 @@ "locked": "L\u00e5st", "not_home": "Borte", "off": "Av", - "ok": "", "on": "P\u00e5", "open": "\u00c5pen", - "problem": "", "unlocked": "Ul\u00e5st" } }, diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json index fbe5f881124..850424df514 100644 --- a/homeassistant/components/guardian/translations/no.json +++ b/homeassistant/components/guardian/translations/no.json @@ -8,8 +8,7 @@ "step": { "user": { "data": { - "ip_address": "IP adresse", - "port": "" + "ip_address": "IP adresse" }, "description": "Konfigurer en lokal Elexa Guardian-enhet." }, diff --git a/homeassistant/components/hassio/translations/nb.json b/homeassistant/components/hassio/translations/nb.json deleted file mode 100644 index d8a4c453015..00000000000 --- a/homeassistant/components/hassio/translations/nb.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "" -} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json deleted file mode 100644 index d8a4c453015..00000000000 --- a/homeassistant/components/hassio/translations/no.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "" -} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/it.json b/homeassistant/components/hlk_sw16/translations/it.json new file mode 100644 index 00000000000..e9356485e08 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/lb.json b/homeassistant/components/hlk_sw16/translations/lb.json new file mode 100644 index 00000000000..e235a2f9047 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwuert", + "username": "Benotzernumm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/ru.json b/homeassistant/components/hlk_sw16/translations/ru.json index ee2788cea56..6f71ee41376 100644 --- a/homeassistant/components/hlk_sw16/translations/ru.json +++ b/homeassistant/components/hlk_sw16/translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/hlk_sw16/translations/zh-Hant.json b/homeassistant/components/hlk_sw16/translations/zh-Hant.json new file mode 100644 index 00000000000..8bf65ef6ee6 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/nb.json b/homeassistant/components/homeassistant/translations/nb.json deleted file mode 100644 index d8a4c453015..00000000000 --- a/homeassistant/components/homeassistant/translations/nb.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "" -} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json deleted file mode 100644 index d8a4c453015..00000000000 --- a/homeassistant/components/homeassistant/translations/no.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "" -} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json deleted file mode 100644 index d8a4c453015..00000000000 --- a/homeassistant/components/homeassistant/translations/pt.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "" -} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index bf19b9af672..bf46d584917 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -15,11 +15,20 @@ "max_peers_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 no t\u00e9 suficient espai lliure.", "max_tries_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 ha rebut m\u00e9s de 100 intents d'autenticaci\u00f3 fallits.", "pairing_failed": "S'ha produ\u00eft un error mentre s'intentava la vinculaci\u00f3 amb aquest dispositiu. Pot ser que sigui un error temporal o pot ser que el teu dispositiu encara no sigui compatible.", + "protocol_error": "Error en comunicar-se amb l'accessori. \u00c9s possible que el dispositiu no estigui en mode de vinculaci\u00f3, potser cal pr\u00e9mer un bot\u00f3 f\u00edsic o virtual.", "unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.", "unknown_error": "El dispositiu ha em\u00e8s un error desconegut. Vinculaci\u00f3 fallida." }, "flow_title": "Accessori HomeKit: {name}", "step": { + "busy_error": { + "description": "Atura la vinculaci\u00f3 a tots els controladors o prova de reiniciar el dispositiu, despr\u00e9s, segueix amb la vinculaci\u00f3.", + "title": "El dispositiu ja s'est\u00e0 vinculant amb un altre controlador" + }, + "max_tries_error": { + "description": "El dispositiu ha rebut m\u00e9s de 100 intents d'autenticaci\u00f3 fallits. Prova de reiniciar el dispositiu, despr\u00e9s torna a intentar vincular-lo.", + "title": "S'ha superat el m\u00e0xim nombre d'intents d'autenticaci\u00f3" + }, "pair": { "data": { "pairing_code": "Codi de vinculaci\u00f3" @@ -27,6 +36,14 @@ "description": "Introdueix el codi de vinculaci\u00f3 de HomeKit per utilitzar aquest accessori (format XXX-XX-XXX)", "title": "Vinculaci\u00f3 amb" }, + "protocol_error": { + "description": "\u00c9s possible que el dispositiu no estigui en mode de vinculaci\u00f3, potser cal pr\u00e9mer un bot\u00f3 f\u00edsic o virtual. Assegura't que el dispositiu est\u00e0 en mode vinculaci\u00f3 o prova de reiniciar-lo, despr\u00e9s, segueix amb la vinculaci\u00f3.", + "title": "Error en comunicar-se amb l'accessori" + }, + "try_pair_later": { + "description": "Assegura't que el dispositiu estigui en mode vinculaci\u00f3 o prova de reiniciar-lo, despr\u00e9s continua reiniciant la vinculaci\u00f3.", + "title": "Vinculaci\u00f3 no disponible" + }, "user": { "data": { "device": "Dispositiu" diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index bc07b71fa75..544c86391be 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -1,50 +1,57 @@ { - "title": "HomeKit Controller", - "config": { - "flow_title": "HomeKit Accessory: {name}", - "step": { - "user": { - "title": "Pair with HomeKit Accessory", - "description": "Select the device you want to pair with", - "data": { - "device": "Device" + "config": { + "abort": { + "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", + "already_configured": "Accessory is already configured with this controller.", + "already_in_progress": "Config flow for device is already in progress.", + "already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.", + "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", + "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", + "no_devices": "No unpaired devices could be found" + }, + "error": { + "authentication_error": "Incorrect HomeKit code. Please check it and try again.", + "busy_error": "Device refused to add pairing as it is already pairing with another controller.", + "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", + "max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.", + "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.", + "protocol_error": "Error communicating with the accessory. Device may not be in pairing mode and may require a physical or virtual button press.", + "unable_to_pair": "Unable to pair, please try again.", + "unknown_error": "Device reported an unknown error. Pairing failed." + }, + "flow_title": "HomeKit Accessory: {name}", + "step": { + "busy_error": { + "description": "Abort pairing on all controllers, or try restarting the device, then continue to resume pairing.", + "title": "The device is already pairing with another controller" + }, + "max_tries_error": { + "description": "The device has received more than 100 unsuccessful authentication attempts. Try restarting the device, then continue to resume pairing.", + "title": "Maximum authentication attempts exceeded" + }, + "pair": { + "data": { + "pairing_code": "Pairing Code" + }, + "description": "Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory", + "title": "Pair with HomeKit Accessory" + }, + "protocol_error": { + "description": "The device may not be in pairing mode and may require a physical or virtual button press. Ensure the device is in pairing mode or try restarting the device, then continue to resume pairing.", + "title": "Error communicating with the accessory" + }, + "try_pair_later": { + "description": "Ensure the device is in pairing mode or try restarting the device, then continue to re-start pairing.", + "title": "Pairing Unavailable" + }, + "user": { + "data": { + "device": "Device" + }, + "description": "Select the device you want to pair with", + "title": "Pair with HomeKit Accessory" + } } - }, - "pair": { - "title": "Pair with HomeKit Accessory", - "description": "Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory", - "data": { - "pairing_code": "Pairing Code" - } - }, - "protocol_error": { - "title": "Error communicating with the accessory", - "description": "The device may not be in pairing mode and may require a physical or virtual button press. Ensure the device is in pairing mode or try restarting the device, then continue to resume pairing." - }, - "busy_error": { - "title": "The device is already pairing with another controller", - "description": "Abort pairing on all controllers, or try restarting the device, then continue to resume pairing." - }, - "max_tries_error": { - "title": "Maximum authentication attempts exceeded", - "description": "The device has received more than 100 unsuccessful authentication attempts. Try restarting the device, then continue to resume pairing." - } }, - "error": { - "unable_to_pair": "Unable to pair, please try again.", - "unknown_error": "Device reported an unknown error. Pairing failed.", - "authentication_error": "Incorrect HomeKit code. Please check it and try again.", - "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", - "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently." - }, - "abort": { - "no_devices": "No unpaired devices could be found", - "already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.", - "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", - "already_configured": "Accessory is already configured with this controller.", - "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", - "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", - "already_in_progress": "Config flow for device is already in progress." - } - } -} + "title": "HomeKit Controller" +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index b48ec79e9db..8eb450e6558 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -15,11 +15,20 @@ "max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.", "max_tries_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que ha recibido m\u00e1s de 100 intentos de autenticaci\u00f3n fallidos.", "pairing_failed": "Se ha producido un error no controlado al intentar emparejarse con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no est\u00e9 admitido en este momento.", + "protocol_error": "Error de comunicaci\u00f3n con el accesorio. El dispositivo puede no estar en modo de emparejamiento y puede requerir una pulsaci\u00f3n de bot\u00f3n f\u00edsica o virtual.", "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. La vinculaci\u00f3n ha fallado." }, "flow_title": "Accesorio HomeKit: {name}", "step": { + "busy_error": { + "description": "Interrumpe el emparejamiento en todos los controladores o intenta reiniciar el dispositivo y luego contin\u00faa con el emparejamiento.", + "title": "El dispositivo ya est\u00e1 emparejando con otro controlador" + }, + "max_tries_error": { + "description": "El dispositivo ha recibido m\u00e1s de 100 intentos de autenticaci\u00f3n fallidos. Intenta reiniciar el dispositivo, luego contin\u00faa para reanudar el emparejamiento.", + "title": "M\u00e1ximo n\u00famero de intentos de autenticaci\u00f3n superados" + }, "pair": { "data": { "pairing_code": "C\u00f3digo de vinculaci\u00f3n" @@ -27,6 +36,14 @@ "description": "Introduce tu c\u00f3digo de vinculaci\u00f3n de HomeKit (en este formato XXX-XX-XXX) para usar este accesorio", "title": "Vincular con accesorio HomeKit" }, + "protocol_error": { + "description": "Es posible que el dispositivo no est\u00e9 en modo de emparejamiento y que requiera que se presione un bot\u00f3n f\u00edsico o virtual. Aseg\u00farate de que el dispositivo est\u00e1 en modo de emparejamiento o intenta reiniciar el dispositivo, luego contin\u00faa para reanudar el emparejamiento.", + "title": "Error al comunicarse con el accesorio" + }, + "try_pair_later": { + "description": "Aseg\u00farate de que el dispositivo est\u00e1 en modo de emparejamiento o reinicia el dispositivo y vuelve a intentar el emparejamiento.", + "title": "Emparejamiento no disponible" + }, "user": { "data": { "device": "Dispositivo" diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index a21ee3a53b3..6a0385737ce 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -27,6 +27,10 @@ "description": "Entrez votre code de jumelage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire.", "title": "Appairer avec l'accessoire HomeKit" }, + "protocol_error": { + "description": "L\u2019appareil peut ne pas \u00eatre en mode appairement et peut n\u00e9cessiter une pression sur un bouton physique ou virtuel. Assurez-vous que l\u2019appareil est en mode appariement ou essayez de red\u00e9marrer l\u2019appareil, puis continuez \u00e0 reprendre l\u2019appariement.", + "title": "Erreur de communication avec l\u2019accessoire" + }, "user": { "data": { "device": "Appareil" diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index 033e6f937da..f07fd6df8b0 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -15,11 +15,20 @@ "max_peers_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento in quanto non dispone di una memoria libera per esso.", "max_tries_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento poich\u00e9 ha ricevuto pi\u00f9 di 100 tentativi di autenticazione non riusciti.", "pairing_failed": "Si \u00e8 verificato un errore non gestito durante il tentativo di abbinamento con questo dispositivo. Potrebbe trattarsi di un errore temporaneo o il dispositivo potrebbe non essere attualmente supportato.", + "protocol_error": "Errore di comunicazione con l'accessorio. Il dispositivo potrebbe non essere in modalit\u00e0 di associazione e potrebbe richiedere la pressione di un pulsante fisico o virtuale.", "unable_to_pair": "Impossibile abbinare, per favore riprova.", "unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito." }, "flow_title": "Accessorio HomeKit: {name}", "step": { + "busy_error": { + "description": "Interrompere l'associazione su tutti i controller o provare a riavviare il dispositivo, quindi continuare a riprendere l'associazione.", + "title": "Il dispositivo \u00e8 gi\u00e0 associato a un altro controller" + }, + "max_tries_error": { + "description": "Il dispositivo ha ricevuto pi\u00f9 di 100 tentativi di autenticazione non riusciti. Prova a riavviare il dispositivo, quindi continua a riprendere l'associazione.", + "title": "Numero massimo di tentativi di autenticazione superato" + }, "pair": { "data": { "pairing_code": "Codice di abbinamento" @@ -27,6 +36,14 @@ "description": "Immettere il codice di abbinamento HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio", "title": "Abbina con accessorio HomeKit" }, + "protocol_error": { + "description": "Il dispositivo potrebbe non essere in modalit\u00e0 di associazione e potrebbe richiedere una pressione di un pulsante fisico o virtuale. Assicurati che il dispositivo sia in modalit\u00e0 di associazione o prova a riavviarlo, quindi continua a riprendere l'associazione.", + "title": "Errore di comunicazione con l'accessorio" + }, + "try_pair_later": { + "description": "Verificare che il dispositivo sia in modalit\u00e0 di associazione o provare a riavviarlo, quindi continuare ad avviare nuovamente l'associazione.", + "title": "Associazione non disponibile" + }, "user": { "data": { "device": "Dispositivo" diff --git a/homeassistant/components/homekit_controller/translations/ko.json b/homeassistant/components/homekit_controller/translations/ko.json index 6cd6c20aad0..55c5ee0053d 100644 --- a/homeassistant/components/homekit_controller/translations/ko.json +++ b/homeassistant/components/homekit_controller/translations/ko.json @@ -20,6 +20,14 @@ }, "flow_title": "HomeKit \uc561\uc138\uc11c\ub9ac: {name}", "step": { + "busy_error": { + "description": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864\ub7ec\uc5d0\uc11c \ud398\uc5b4\ub9c1\uc744 \uc911\ub2e8\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.", + "title": "\uae30\uae30\uac00 \uc774\ubbf8 \ub2e4\ub978 \ucee8\ud2b8\ub864\ub7ec\uc640 \ud398\uc774\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4" + }, + "max_tries_error": { + "description": "\uae30\uae30\uac00 100\ud68c \uc774\uc0c1\uc758 \uc2e4\ud328\ud55c \uc778\uc99d \uc2dc\ub3c4\ub97c \ubc1b\uc558\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.", + "title": "\ucd5c\ub300 \uc778\uc99d \uc2dc\ub3c4 \ud69f\uc218\ub97c \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4" + }, "pair": { "data": { "pairing_code": "\ud398\uc5b4\ub9c1 \ucf54\ub4dc" @@ -27,6 +35,10 @@ "description": "\uc774 \uc561\uc138\uc11c\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 HomeKit \ud398\uc5b4\ub9c1 \ucf54\ub4dc (XXX-XX-XXX \ud615\uc2dd) \ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "HomeKit \uc561\uc138\uc11c\ub9ac \ud398\uc5b4\ub9c1\ud558\uae30" }, + "protocol_error": { + "description": "\uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\uc9c0 \uc54a\uc744 \uc218 \uc788\uc73c\uba70 \ubb3c\ub9ac\uc801 \ub610\ub294 \uac00\uc0c1 \uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc57c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uae30\uae30\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc\uc5d0 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uac70\ub098 \uae30\uae30\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud55c \ub2e4\uc74c \ud398\uc5b4\ub9c1\uc744 \uacc4\uc18d\ud574\uc8fc\uc138\uc694.", + "title": "\uc561\uc138\uc11c\ub9ac\uc640 \ud1b5\uc2e0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, "user": { "data": { "device": "\uae30\uae30" diff --git a/homeassistant/components/homekit_controller/translations/lb.json b/homeassistant/components/homekit_controller/translations/lb.json index c840cb4e9fc..5431b3b30d1 100644 --- a/homeassistant/components/homekit_controller/translations/lb.json +++ b/homeassistant/components/homekit_controller/translations/lb.json @@ -15,11 +15,20 @@ "max_peers_error": "Den Apparat huet den Kupplungs Versuch refus\u00e9iert well et keng fr\u00e4i Pairing Memoire huet.", "max_tries_error": "Den Apparat huet den Kupplungs Versuch refus\u00e9iert well et m\u00e9i w\u00e9i 100 net erfollegr\u00e4ich Authentifikatioun's Versich erhalen huet.", "pairing_failed": "Eng onerwaarte Feeler ass opgetruede beim Kupplung's Versuch mat d\u00ebsem Apparat. D\u00ebst kann e tempor\u00e4re Feeler sinn oder D\u00e4in Apparat g\u00ebtt aktuell net \u00ebnnerst\u00ebtzt.", + "protocol_error": "Feeler bei der Kommunikatioun mam Accessoire. Apparat ass vill\u00e4icht net am Kopplungs Modus a ben\u00e9idegt een Drock op ee Kn\u00e4ppchen.", "unable_to_pair": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.", "unknown_error": "Apparat mellt een onbekannte Feeler. Verbindung net m\u00e9iglech." }, "flow_title": "HomeKit Accessoire: {name}", "step": { + "busy_error": { + "description": "Briech d'Kopplung op all Kontroller of, oder start den Apparat fr\u00ebsch, da versich et nach emol mat der Kopplung.", + "title": "Den Apparat ass am gaange mat engem anere Kontroller ze koppelen." + }, + "max_tries_error": { + "description": "Den Apparat huet m\u00e9i w\u00e9i 100 net erfollegr\u00e4ich Authentifikatioun Versich. Prob\u00e9ier den Apparat nei ze starten, da versich et nach emol mat der Kopplung.", + "title": "Maximum vun den Authentifikatioun Versich iwwerschratt" + }, "pair": { "data": { "pairing_code": "Pairing Code" @@ -27,6 +36,14 @@ "description": "Gitt \u00e4ren HomeKit pairing Code (am Format XXX-XX-XXX) an fir d\u00ebsen Accessoire ze benotzen", "title": "Mam HomeKit Accessoire verbannen" }, + "protocol_error": { + "description": "Den Apparat ass vill\u00e4icht net am Kopplungs Modus an et muss ee Kn\u00e4ppche gedr\u00e9ckt ginn. Stell s\u00e9cher dass den Apparat am Kopplung Modus ass oder prob\u00e9ier den Apparat fr\u00ebsch ze starten, da versich et nach emol mat der Kopplung.", + "title": "Feeler bei der Kommunikatioun mam Accessoire" + }, + "try_pair_later": { + "description": "Stell s\u00e9cher dass den Apparat sech am Kopplungs Modus bef\u00ebnnt oder prob\u00e9ier den Apparat fr\u00ebsch ze starten, da prob\u00e9ier nach emol mat der Kopplung.", + "title": "Kopplung net m\u00e9iglech" + }, "user": { "data": { "device": "Apparat" diff --git a/homeassistant/components/homekit_controller/translations/no.json b/homeassistant/components/homekit_controller/translations/no.json index 72dfeede783..45444d3d9d0 100644 --- a/homeassistant/components/homekit_controller/translations/no.json +++ b/homeassistant/components/homekit_controller/translations/no.json @@ -15,11 +15,20 @@ "max_peers_error": "Enheten nekter \u00e5 sammenkoble da den ikke har ledig sammenkoblingslagring.", "max_tries_error": "Enheten nekter \u00e5 sammenkoble da den har mottatt mer enn 100 mislykkede godkjenningsfors\u00f8k.", "pairing_failed": "En uh\u00e5ndtert feil oppstod under fors\u00f8k p\u00e5 \u00e5 koble til denne enheten. Dette kan v\u00e6re en midlertidig feil, eller at enheten din kan ikke st\u00f8ttes for \u00f8yeblikket.", + "protocol_error": "Feil under kommunikasjon med tilbeh\u00f8ret. Enheten er kanskje ikke i paringsmodus og kan kreve et fysisk eller virtuelt knappetrykk.", "unable_to_pair": "Kunne ikke koble til, vennligst pr\u00f8v igjen.", "unknown_error": "Enheten rapporterte en ukjent feil. Sammenkobling mislyktes." }, "flow_title": "HomeKit Tilbeh\u00f8r: {name}", "step": { + "busy_error": { + "description": "Avbryt sammenkobling p\u00e5 alle kontrollere, eller pr\u00f8v \u00e5 starte enheten p\u00e5 nytt, og fortsett deretter med \u00e5 fortsette sammenkoblingen.", + "title": "Enheten er allerede sammenkoblet med en annen kontroller" + }, + "max_tries_error": { + "description": "Enheten har mottatt mer enn 100 mislykkede godkjenningsfors\u00f8k. Pr\u00f8v \u00e5 starte enheten p\u00e5 nytt, og fortsett deretter \u00e5 fortsette paringen.", + "title": "Maksimalt antall godkjenningsfors\u00f8k overskredet" + }, "pair": { "data": { "pairing_code": "Sammenkoblingskode" @@ -27,6 +36,14 @@ "description": "Angi din HomeKit-sammenkoblingskode (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret", "title": "Koble til HomeKit tilbeh\u00f8r" }, + "protocol_error": { + "description": "Enheten er kanskje ikke i paringsmodus og kan kreve et fysisk eller virtuelt knappetrykk. Kontroller at enheten er i paringsmodus eller pr\u00f8v \u00e5 starte enheten p\u00e5 nytt, og fortsett deretter \u00e5 fortsette paringen.", + "title": "Feil ved kommunikasjon med tilbeh\u00f8ret" + }, + "try_pair_later": { + "description": "Kontroller at enheten er i paringsmodus eller pr\u00f8v \u00e5 starte enheten p\u00e5 nytt, og fortsett deretter \u00e5 starte paringen p\u00e5 nytt.", + "title": "Paring utilgjengelig" + }, "user": { "data": { "device": "Enhet" diff --git a/homeassistant/components/homekit_controller/translations/pt-BR.json b/homeassistant/components/homekit_controller/translations/pt-BR.json index ff0dadf8c57..b420e8eafeb 100644 --- a/homeassistant/components/homekit_controller/translations/pt-BR.json +++ b/homeassistant/components/homekit_controller/translations/pt-BR.json @@ -20,6 +20,12 @@ }, "flow_title": "Acess\u00f3rio HomeKit: {name}", "step": { + "busy_error": { + "title": "O dispositivo est\u00e1 pareando com outro controlador" + }, + "max_tries_error": { + "title": "Quantidade de tentativas de autentica\u00e7\u00e3o excedido" + }, "pair": { "data": { "pairing_code": "C\u00f3digo de pareamento" diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index 95f8a4d6efe..fb9612ef981 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -15,11 +15,20 @@ "max_peers_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u043b\u043e \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0438\u0437-\u0437\u0430 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044f \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u0430.", "max_tries_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u043b\u043e \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u0431\u044b\u043b\u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e \u0431\u043e\u043b\u0435\u0435 100 \u043d\u0435\u0443\u0434\u0430\u0447\u043d\u044b\u0445 \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "pairing_failed": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0441\u0431\u043e\u0439 \u0438\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0435\u0449\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "protocol_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u0435\u0440\u0435\u0432\u0435\u0434\u0435\u043d\u043e \u0432 \u0440\u0435\u0436\u0438\u043c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f. \u0414\u043b\u044f \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0438\u0438 \u0440\u0435\u0436\u0438\u043c\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u0438\u0435 \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0438\u043b\u0438 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0439 \u043a\u043d\u043e\u043f\u043a\u0438.", "unable_to_pair": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", "unknown_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u043e\u043e\u0431\u0449\u0438\u043b\u043e \u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435. \u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c." }, "flow_title": "\u0410\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 HomeKit: {name}", "step": { + "busy_error": { + "description": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0432\u0441\u0435\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430\u0445 \u0438\u043b\u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", + "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u0434\u0440\u0443\u0433\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0443." + }, + "max_tries_error": { + "description": "\u0411\u043e\u043b\u0435\u0435 100 \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0448\u043b\u0438 \u043d\u0435\u0443\u0434\u0430\u0447\u043d\u043e. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0430 \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", + "title": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, "pair": { "data": { "pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" @@ -27,6 +36,14 @@ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit" }, + "protocol_error": { + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u043d\u0435 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u0438 \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u0438\u0435 \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0438\u043b\u0438 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0439 \u043a\u043d\u043e\u043f\u043a\u0438. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f, \u0438\u043b\u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0438 \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", + "title": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c." + }, + "try_pair_later": { + "description": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e" + }, "user": { "data": { "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/homekit_controller/translations/zh-Hans.json b/homeassistant/components/homekit_controller/translations/zh-Hans.json index 56ba2c948a4..3cf753b41f5 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hans.json @@ -20,6 +20,14 @@ }, "flow_title": "HomeKit \u914d\u4ef6: {name}", "step": { + "busy_error": { + "description": "\u4e2d\u6b62\u6240\u6709\u63a7\u5236\u5668\u4e0a\u7684\u914d\u5bf9\uff0c\u6216\u5c1d\u8bd5\u5148\u91cd\u542f\u8bbe\u5907\uff0c\u7136\u540e\u7ee7\u7eed\u914d\u5bf9\u3002", + "title": "\u6b64\u914d\u4ef6\u5df2\u4e0e\u53e6\u4e00\u53f0\u8bbe\u5907\u914d\u5bf9\u3002\u8bf7\u91cd\u7f6e\u914d\u4ef6\uff0c\u7136\u540e\u91cd\u8bd5\u3002" + }, + "max_tries_error": { + "description": "\u8bbe\u5907\u5df2\u6536\u5230100\u591a\u6b21\u5931\u8d25\u7684\u8eab\u4efd\u9a8c\u8bc1\u5c1d\u8bd5\u3002\u5c1d\u8bd5\u91cd\u65b0\u542f\u52a8\u8bbe\u5907\uff0c\u7136\u540e\u7ee7\u7eed\u6062\u590d\u914d\u5bf9\u3002", + "title": "\u8d85\u8fc7\u6700\u5927\u8eab\u4efd\u9a8c\u8bc1\u5c1d\u8bd5\u6b21\u6570" + }, "pair": { "data": { "pairing_code": "\u914d\u5bf9\u4ee3\u7801" @@ -27,6 +35,10 @@ "description": "\u8f93\u5165\u60a8\u7684 HomeKit \u914d\u5bf9\u4ee3\u7801\uff08\u683c\u5f0f\u4e3a XXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", "title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9" }, + "protocol_error": { + "description": "\u8bbe\u5907\u53ef\u80fd\u672a\u5904\u4e8e\u914d\u5bf9\u6a21\u5f0f\uff0c\u53ef\u80fd\u9700\u8981\u6309\u4e0b\u7269\u7406\u6216\u865a\u62df\u6309\u94ae\u3002\u786e\u4fdd\u8bbe\u5907\u5904\u4e8e\u914d\u5bf9\u6a21\u5f0f\uff0c\u6216\u5c1d\u8bd5\u91cd\u65b0\u542f\u52a8\u8bbe\u5907\uff0c\u7136\u540e\u7ee7\u7eed\u6062\u590d\u914d\u5bf9\u3002", + "title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9\u5931\u8d25" + }, "user": { "data": { "device": "\u8bbe\u5907" diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index 27ff273d117..da4b908c840 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -15,11 +15,20 @@ "max_peers_error": "\u8a2d\u5099\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", "max_tries_error": "\u8a2d\u5099\u6536\u5230\u8d85\u904e 100 \u6b21\u672a\u6210\u529f\u8a8d\u8b49\u5f8c\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", "pairing_failed": "\u7576\u8a66\u5716\u8207\u8a2d\u5099\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u8a2d\u5099\u76ee\u524d\u4e0d\u652f\u63f4\u3002", + "protocol_error": "\u8207\u8a2d\u5099\u901a\u8a0a\u6642\u767c\u751f\u932f\u8aa4\u3002\u8a2d\u5099\u53ef\u80fd\u4e26\u672a\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\u3001\u4e26\u9700\u8981\u6309\u4e0b\u5be6\u9ad4\u6216\u865b\u64ec\u6309\u9215\u3002", "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "unknown_error": "\u8a2d\u5099\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, "flow_title": "HomeKit \u914d\u4ef6\uff1a{name}", "step": { + "busy_error": { + "description": "\u53d6\u6d88\u6240\u6709\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u6216\u8005\u91cd\u555f\u8a2d\u5099\u3001\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", + "title": "\u8a2d\u5099\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d" + }, + "max_tries_error": { + "description": "\u8a2d\u5099\u5df2\u8d85\u904e 100 \u6b21\u8a8d\u8b49\u5617\u8a66\u6b21\u6578\uff0c\u8acb\u5617\u8a66\u91cd\u65b0\u555f\u52d5\u8a2d\u5099\u3001\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", + "title": "\u5df2\u8d85\u904e\u6700\u5927\u9a57\u8b49\u5617\u8a66\u6b21\u6578" + }, "pair": { "data": { "pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc" @@ -27,6 +36,14 @@ "description": "\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", "title": "HomeKit \u914d\u4ef6\u914d\u5c0d" }, + "protocol_error": { + "description": "\u8a2d\u5099\u4e26\u672a\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\uff0c\u53ef\u80fd\u9700\u8981\u6309\u4e0b\u5be6\u9ad4\u6216\u865b\u64ec\u6309\u9215\u3002\u8acb\u78ba\u5b9a\u8a2d\u5099\u5df2\u7d93\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\u3001\u6216\u91cd\u555f\u8a2d\u5099\uff0c\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", + "title": "\u8207\u914d\u4ef6\u901a\u8a0a\u932f\u8aa4" + }, + "try_pair_later": { + "description": "\u8acb\u78ba\u5b9a\u8a2d\u5099\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\u4e2d\u6216\u91cd\u555f\u8a2d\u5099\uff0c\u7136\u5f8c\u91cd\u65b0\u958b\u59cb\u914d\u5c0d\u3002", + "title": "\u914d\u5c0d\u7121\u6cd5\u4f7f\u7528" + }, "user": { "data": { "device": "\u8a2d\u5099" diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 5c765c83bb9..86d3f37226f 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "url": "URL \u0410\u0434\u0440\u0435\u0441", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e. \u041f\u043e\u0441\u043e\u0447\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0435 \u0435 \u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e, \u043d\u043e \u0434\u0430\u0432\u0430 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0437\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435. \u041e\u0442 \u0434\u0440\u0443\u0433\u0430 \u0441\u0442\u0440\u0430\u043d\u0430, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0434\u043e\u0432\u0435\u0434\u0435 \u0434\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 \u0434\u043e\u0441\u0442\u044a\u043f\u0430 \u0434\u043e \u0443\u0435\u0431 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043e\u0442\u0432\u044a\u043d Home Assistant, \u0434\u043e\u043a\u0430\u0442\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0442\u043e.", diff --git a/homeassistant/components/huawei_lte/translations/cs.json b/homeassistant/components/huawei_lte/translations/cs.json index 6ba708f2b0e..9593c27d76a 100644 --- a/homeassistant/components/huawei_lte/translations/cs.json +++ b/homeassistant/components/huawei_lte/translations/cs.json @@ -17,7 +17,6 @@ "user": { "data": { "password": "Heslo", - "url": "URL", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, "title": "Konfigurovat Huawei LTE" diff --git a/homeassistant/components/huawei_lte/translations/da.json b/homeassistant/components/huawei_lte/translations/da.json index e07499b0073..9f377fba47a 100644 --- a/homeassistant/components/huawei_lte/translations/da.json +++ b/homeassistant/components/huawei_lte/translations/da.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Adgangskode", - "url": "Webadresse", "username": "Brugernavn" }, "description": "Indtast oplysninger om enhedsadgang. Det er valgfrit at specificere brugernavn og adgangskode, men muligg\u00f8r underst\u00f8ttelse af flere integrationsfunktioner. P\u00e5 den anden side kan brug af en autoriseret forbindelse for\u00e5rsage problemer med at f\u00e5 adgang til enhedens webgr\u00e6nseflade uden for Home Assistant, mens integrationen er aktiv, og omvendt.", diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index d3a9aa3efbc..15fd57a3d33 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Passwort", - "url": "URL", "username": "Benutzername" }, "description": "Gib die Zugangsdaten zum Ger\u00e4t ein. Die Angabe von Benutzername und Passwort ist optional, erm\u00f6glicht aber die Unterst\u00fctzung weiterer Integrationsfunktionen. Andererseits kann die Verwendung einer autorisierten Verbindung zu Problemen beim Zugriff auf die Web-Schnittstelle des Ger\u00e4ts von au\u00dferhalb des Home Assistant f\u00fchren, w\u00e4hrend die Integration aktiv ist, und umgekehrt.", diff --git a/homeassistant/components/huawei_lte/translations/es-419.json b/homeassistant/components/huawei_lte/translations/es-419.json index a00f805c9fa..5802d36b348 100644 --- a/homeassistant/components/huawei_lte/translations/es-419.json +++ b/homeassistant/components/huawei_lte/translations/es-419.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Contrase\u00f1a", - "url": "URL", "username": "Nombre de usuario" }, "description": "Ingrese los detalles de acceso del dispositivo. Especificar nombre de usuario y contrase\u00f1a es opcional, pero habilita el soporte para m\u00e1s funciones de integraci\u00f3n. Por otro lado, el uso de una conexi\u00f3n autorizada puede causar problemas para acceder a la interfaz web del dispositivo desde fuera de Home Assistant mientras la integraci\u00f3n est\u00e1 activa y viceversa.", diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index 2c7437d9a14..5270168d9ca 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -21,7 +21,6 @@ "user": { "data": { "password": "Mot de passe", - "url": "URL", "username": "Nom d'utilisateur" }, "description": "Entrez les d\u00e9tails d'acc\u00e8s au p\u00e9riph\u00e9rique. La sp\u00e9cification du nom d'utilisateur et du mot de passe est facultative, mais permet de prendre en charge davantage de fonctionnalit\u00e9s d'int\u00e9gration. En revanche, l\u2019utilisation d\u2019une connexion autoris\u00e9e peut entra\u00eener des probl\u00e8mes d\u2019acc\u00e8s \u00e0 l\u2019interface Web du p\u00e9riph\u00e9rique depuis l\u2019assistant externe lorsque l\u2019int\u00e9gration est active et inversement.", diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 485a29f5a77..ec5aa78aefd 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -17,7 +17,6 @@ "user": { "data": { "password": "Jelsz\u00f3", - "url": "URL", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "title": "Huawei LTE konfigur\u00e1l\u00e1sa" diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index 79c08dfa66d..fd26e454d33 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -21,7 +21,6 @@ "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", - "url": "URL \uc8fc\uc18c", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "\uae30\uae30 \uc561\uc138\uc2a4 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d\ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant \uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/huawei_lte/translations/lv.json b/homeassistant/components/huawei_lte/translations/lv.json index e276ee03f24..2c205bdd324 100644 --- a/homeassistant/components/huawei_lte/translations/lv.json +++ b/homeassistant/components/huawei_lte/translations/lv.json @@ -4,7 +4,6 @@ "user": { "data": { "password": "Parole", - "url": "URL", "username": "Lietot\u0101jv\u0101rds" } } diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index 0a1ebbf2ea2..d422122185e 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Wachtwoord", - "url": "URL", "username": "Gebruikersnaam" }, "description": "Voer de toegangsgegevens van het apparaat in. Opgeven van gebruikersnaam en wachtwoord is optioneel, maar biedt ondersteuning voor meer integratiefuncties. Aan de andere kant kan het gebruik van een geautoriseerde verbinding problemen veroorzaken bij het openen van het webinterface van het apparaat buiten de Home Assitant, terwijl de integratie actief is en andersom.", diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index f22681313d6..1cc80845ea6 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -16,12 +16,11 @@ "response_error": "Ukjent feil fra enheten", "unknown_connection_error": "Ukjent feil under tilkobling til enhet" }, - "flow_title": "", "step": { "user": { "data": { "password": "Passord", - "url": "", + "url": "URL", "username": "Brukernavn" }, "description": "Fyll inn detaljer for enhetstilgang. Spesifisering av brukernavn og passord er valgfritt, men gir st\u00f8tte for flere integrasjonsfunksjoner. P\u00e5 en annen side kan bruk av en autorisert tilkobling f\u00f8re til problemer med tilgang til enhetens webgrensesnitt utenfor Home Assistant mens integrasjonen er aktiv, og omvendt.", diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 405ffdf0343..e38188d134f 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -21,7 +21,6 @@ "user": { "data": { "password": "Has\u0142o", - "url": "URL", "username": "Nazwa u\u017cytkownika" }, "description": "Wprowad\u017a szczeg\u00f3\u0142y dost\u0119pu do urz\u0105dzenia. Okre\u015blenie nazwy u\u017cytkownika i has\u0142a jest opcjonalne, ale umo\u017cliwia obs\u0142ug\u0119 wi\u0119kszej liczby funkcji integracji. Z drugiej strony u\u017cycie autoryzowanego po\u0142\u0105czenia mo\u017ce powodowa\u0107 problemy z dost\u0119pem do interfejsu internetowego urz\u0105dzenia z zewn\u0105trz Home Assistanta gdy integracja jest aktywna.", diff --git a/homeassistant/components/huawei_lte/translations/pt-BR.json b/homeassistant/components/huawei_lte/translations/pt-BR.json new file mode 100644 index 00000000000..c69337fa2bb --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo j\u00e1 foi configurado" + }, + "step": { + "user": { + "data": { + "url": "URL", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/pt.json b/homeassistant/components/huawei_lte/translations/pt.json index b92752d7d13..f43cf3acc4f 100644 --- a/homeassistant/components/huawei_lte/translations/pt.json +++ b/homeassistant/components/huawei_lte/translations/pt.json @@ -15,7 +15,6 @@ "user": { "data": { "password": "Palavra-passe", - "url": "", "username": "Nome do utilizador" }, "title": "Configurar o Huawei LTE" diff --git a/homeassistant/components/huawei_lte/translations/sl.json b/homeassistant/components/huawei_lte/translations/sl.json index fe4a8db5d06..8898a9123df 100644 --- a/homeassistant/components/huawei_lte/translations/sl.json +++ b/homeassistant/components/huawei_lte/translations/sl.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "Geslo", - "url": "URL", "username": "Uporabni\u0161ko ime" }, "description": "Vnesite podatke za dostop do naprave. Dolo\u010danje uporabni\u0161kega imena in gesla je izbirno, vendar omogo\u010da podporo za ve\u010d funkcij integracije. Po drugi strani pa lahko uporaba poobla\u0161\u010dene povezave povzro\u010di te\u017eave pri dostopu do spletnega vmesnika naprave zunaj Home Assistant-a, medtem ko je integracija aktivna, in obratno.", diff --git a/homeassistant/components/huawei_lte/translations/sv.json b/homeassistant/components/huawei_lte/translations/sv.json index 3dd267ed731..15af95e6e9e 100644 --- a/homeassistant/components/huawei_lte/translations/sv.json +++ b/homeassistant/components/huawei_lte/translations/sv.json @@ -20,7 +20,6 @@ "user": { "data": { "password": "L\u00f6senord", - "url": "URL", "username": "Anv\u00e4ndarnamn" }, "description": "Ange information om enhets\u00e5tkomst. Det \u00e4r valfritt att ange anv\u00e4ndarnamn och l\u00f6senord, men st\u00f6djer d\u00e5 fler integrationsfunktioner. \u00c5 andra sidan kan anv\u00e4ndning av en auktoriserad anslutning orsaka problem med att komma \u00e5t enhetens webbgr\u00e4nssnitt utanf\u00f6r Home Assistant medan integrationen \u00e4r aktiv och tv\u00e4rtom.", diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json index 92553c84cfe..e7ff3c415fb 100644 --- a/homeassistant/components/hue/translations/et.json +++ b/homeassistant/components/hue/translations/et.json @@ -2,13 +2,6 @@ "config": { "abort": { "unknown": "Ilmnes tundmatu viga" - }, - "step": { - "init": { - "data": { - "host": "" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/no.json b/homeassistant/components/hue/translations/no.json index 2d51ee26452..dbef1a20d48 100644 --- a/homeassistant/components/hue/translations/no.json +++ b/homeassistant/components/hue/translations/no.json @@ -22,8 +22,7 @@ "title": "Velg Hue Bridge" }, "link": { - "description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n ![Knappens plassering p\u00e5 Bridgen](/static/images/config_philips_hue.jpg)", - "title": "" + "description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n ![Knappens plassering p\u00e5 Bridgen](/static/images/config_philips_hue.jpg)" }, "manual": { "data": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pt-BR.json b/homeassistant/components/hunterdouglas_powerview/translations/pt-BR.json new file mode 100644 index 00000000000..f7dc708a2d6 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/pt-BR.json b/homeassistant/components/iaqualink/translations/pt-BR.json new file mode 100644 index 00000000000..932b4b8a72e --- /dev/null +++ b/homeassistant/components/iaqualink/translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/image_processing/translations/pt-BR.json b/homeassistant/components/image_processing/translations/pt-BR.json index 96c31ffda19..77c6a8eb2cc 100644 --- a/homeassistant/components/image_processing/translations/pt-BR.json +++ b/homeassistant/components/image_processing/translations/pt-BR.json @@ -1,3 +1,3 @@ { - "title": "Processamento de imagem" + "title": "Processando imagem" } \ No newline at end of file diff --git a/homeassistant/components/input_boolean/translations/cs.json b/homeassistant/components/input_boolean/translations/cs.json index 3db899fd093..4a7d1a8e8c3 100644 --- a/homeassistant/components/input_boolean/translations/cs.json +++ b/homeassistant/components/input_boolean/translations/cs.json @@ -5,5 +5,5 @@ "on": "Aktivn\u00ed" } }, - "title": "Zad\u00e1n\u00ed ano/ne" + "title": "Pomocn\u00edci - p\u0159ep\u00edna\u010de" } \ No newline at end of file diff --git a/homeassistant/components/input_datetime/translations/cs.json b/homeassistant/components/input_datetime/translations/cs.json index 810b63fdcf9..643c42f1111 100644 --- a/homeassistant/components/input_datetime/translations/cs.json +++ b/homeassistant/components/input_datetime/translations/cs.json @@ -1,3 +1,3 @@ { - "title": "Zad\u00e1n\u00ed \u010dasu" + "title": "Pomocn\u00edci - data/\u010dasy" } \ No newline at end of file diff --git a/homeassistant/components/input_number/translations/cs.json b/homeassistant/components/input_number/translations/cs.json index c639bae22f0..a750598ed57 100644 --- a/homeassistant/components/input_number/translations/cs.json +++ b/homeassistant/components/input_number/translations/cs.json @@ -1,3 +1,3 @@ { - "title": "Zad\u00e1n\u00ed \u010d\u00edsla" + "title": "Pomocn\u00edci - \u010d\u00edsla" } \ No newline at end of file diff --git a/homeassistant/components/input_select/translations/cs.json b/homeassistant/components/input_select/translations/cs.json index c36a4687fda..32a9bef0556 100644 --- a/homeassistant/components/input_select/translations/cs.json +++ b/homeassistant/components/input_select/translations/cs.json @@ -1,3 +1,3 @@ { - "title": "Zad\u00e1n\u00ed volby" + "title": "Pomocn\u00edci - v\u00fdb\u011bry" } \ No newline at end of file diff --git a/homeassistant/components/input_text/translations/cs.json b/homeassistant/components/input_text/translations/cs.json index a0e09f9fe84..9e14dfd0072 100644 --- a/homeassistant/components/input_text/translations/cs.json +++ b/homeassistant/components/input_text/translations/cs.json @@ -1,3 +1,3 @@ { - "title": "Zad\u00e1n\u00ed textu" + "title": "Pomocn\u00edci - texty" } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/ca.json b/homeassistant/components/insteon/translations/ca.json new file mode 100644 index 00000000000..859ba47b79e --- /dev/null +++ b/homeassistant/components/insteon/translations/ca.json @@ -0,0 +1,115 @@ +{ + "config": { + "abort": { + "already_configured": "Ja hi ha una connexi\u00f3 amb un m\u00f2dem Insteon configurada", + "cannot_connect": "No es pot connectar amb el m\u00f2dem Insteon" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar al m\u00f2dem Insteon, torna-ho a provar.", + "select_single": "Selecciona una opci\u00f3." + }, + "step": { + "hub1": { + "data": { + "host": "Adre\u00e7a IP del Hub", + "port": "Port IP" + }, + "description": "Configura l'Insteon Hub versi\u00f3 1 (anterior a 2014).", + "title": "Insteon Hub versi\u00f3 1" + }, + "hub2": { + "data": { + "host": "Adre\u00e7a IP del Hub", + "password": "Contrasenya", + "port": "Port IP", + "username": "Nom d'usuari" + }, + "description": "Configura l'Insteon Hub versi\u00f3 2.", + "title": "Insteon Hub versi\u00f3 2" + }, + "init": { + "data": { + "hubv1": "Hub versi\u00f3 1 (anterior a 2014)", + "hubv2": "Hub versi\u00f3 2", + "plm": "M\u00f2dem PowerLink (PLM)" + }, + "description": "Selecciona el tipus de m\u00f2dem Insteon.", + "title": "Insteon" + }, + "plm": { + "data": { + "device": "Dispositiu PLM (ex: /dev/ttyUSB0 o COM3)" + }, + "description": "Configura el m\u00f2dem Insteon PowerLink (PLM).", + "title": "Insteon PLM" + } + } + }, + "options": { + "abort": { + "already_configured": "Ja hi ha una connexi\u00f3 amb un m\u00f2dem Insteon configurada", + "cannot_connect": "No es pot connectar amb el m\u00f2dem Insteon" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar al m\u00f2dem Insteon, torna-ho a provar.", + "input_error": "Entrades inv\u00e0lides, revisa els valors.", + "select_single": "Selecciona una opci\u00f3." + }, + "step": { + "add_override": { + "data": { + "address": "Adre\u00e7a del dispositiu (ex: 1a2b3c)", + "cat": "Categoria del dispositiu (ex: 0x10)", + "subcat": "Subcategoria del dispositiu (ex: 0x0a)" + }, + "description": "Afegeix una substituci\u00f3 de dispositiu.", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "Housecode (a - p)", + "platform": "Plataforma", + "steps": "Passos del regulador d'intensitat (nom\u00e9s per a llums, 22 per defecte)", + "unitcode": "Unitcode (1-16)" + }, + "description": "Canvia la contrasenya de l'Insteon Hub.", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "Nou nom d'amfitri\u00f3 o adre\u00e7a IP", + "password": "Nova contrasenya", + "port": "Nou n\u00famero de port", + "username": "Nou nom d'usuari" + }, + "description": "Canvia la informaci\u00f3 de connexi\u00f3 de l'Insteon Hub. Has de reiniciar Home Assistant si fas canvis. Aix\u00f2 no canvia la configuraci\u00f3 del Hub en si. Per canviar la configuraci\u00f3 del Hub, utilitza la seva aplicaci\u00f3.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Afegeix una substituci\u00f3 de dispositiu.", + "add_x10": "Afegeix un dispositiu X10.", + "change_hub_config": "Canvia la configuraci\u00f3 del Hub.", + "remove_override": "Elimina una substituci\u00f3 de dispositiu.", + "remove_x10": "Elimina un dispositiu X10." + }, + "description": "Selecciona una opci\u00f3 a configurar.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Selecciona una adre\u00e7a de dispositiu a eliminar" + }, + "description": "Elimina una substituci\u00f3 de dispositiu", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "Selecciona una adre\u00e7a de dispositiu a eliminar" + }, + "description": "Elimina un dispositiu X10", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json new file mode 100644 index 00000000000..9bdec0e7fcc --- /dev/null +++ b/homeassistant/components/insteon/translations/es.json @@ -0,0 +1,115 @@ +{ + "config": { + "abort": { + "already_configured": "Una conexi\u00f3n de m\u00f3dem Insteon ya est\u00e1 configurada", + "cannot_connect": "No se puede conectar al m\u00f3dem Insteon" + }, + "error": { + "cannot_connect": "No se conect\u00f3 al m\u00f3dem Insteon, por favor, int\u00e9ntelo de nuevo.", + "select_single": "Seleccione una opci\u00f3n." + }, + "step": { + "hub1": { + "data": { + "host": "Direcci\u00f3n IP del Hub", + "port": "Puerto IP" + }, + "description": "Configure el Insteon Hub Versi\u00f3n 1 (anterior a 2014).", + "title": "Insteon Hub Versi\u00f3n 1" + }, + "hub2": { + "data": { + "host": "Direcci\u00f3n IP del Hub", + "password": "Contrase\u00f1a", + "port": "Puerto IP", + "username": "Nombre de usuario" + }, + "description": "Configure el Insteon Hub versi\u00f3n 2.", + "title": "Insteon Hub Versi\u00f3n 2" + }, + "init": { + "data": { + "hubv1": "Hub versi\u00f3n 1 (anterior a 2014)", + "hubv2": "Hub Versi\u00f3n 2", + "plm": "M\u00f3dem PowerLink (PLM)" + }, + "description": "Seleccione el tipo de m\u00f3dem Insteon.", + "title": "Insteon" + }, + "plm": { + "data": { + "device": "Dispositivo PLM (es decir, /dev/ttyUSB0 o COM3)" + }, + "description": "Configure el M\u00f3dem Insteon PowerLink (PLM).", + "title": "Insteon PLM" + } + } + }, + "options": { + "abort": { + "already_configured": "Ya est\u00e1 configurada una conexi\u00f3n del m\u00f3dem Insteon", + "cannot_connect": "No se puede conectar al m\u00f3dem Insteon" + }, + "error": { + "cannot_connect": "No se conect\u00f3 al m\u00f3dem Insteon, por favor, int\u00e9ntelo de nuevo.", + "input_error": "Entradas no v\u00e1lidas, compruebe sus valores.", + "select_single": "Selecciona una opci\u00f3n" + }, + "step": { + "add_override": { + "data": { + "address": "Direcci\u00f3n del dispositivo (es decir, 1a2b3c)", + "cat": "Categor\u00eda del dispositivo (es decir, 0x10)", + "subcat": "Subcategor\u00eda del dispositivo (es decir, 0x0a)" + }, + "description": "Agregue una anulaci\u00f3n del dispositivo.", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "C\u00f3digo de casa (a - p)", + "platform": "Plataforma", + "steps": "Pasos de atenuaci\u00f3n (s\u00f3lo para dispositivos de luz, por defecto 22)", + "unitcode": "Unitcode (1 - 16)" + }, + "description": "Cambie la contrase\u00f1a del Hub Insteon.", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "Nuevo nombre del host o direcci\u00f3n IP", + "password": "Nueva contrase\u00f1a", + "port": "Nuevo n\u00famero de puerto", + "username": "Nuevo nombre de usuario" + }, + "description": "Cambiar la informaci\u00f3n de la conexi\u00f3n del Hub Insteon. Debes reiniciar el Home Assistant despu\u00e9s de hacer este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n del Hub usa la aplicaci\u00f3n Hub.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Agregue una anulaci\u00f3n del dispositivo.", + "add_x10": "A\u00f1ade un dispositivo X10.", + "change_hub_config": "Cambie la configuraci\u00f3n del Hub.", + "remove_override": "Eliminar una anulaci\u00f3n del dispositivo.", + "remove_x10": "Eliminar un dispositivo X10" + }, + "description": "Seleccione una opci\u00f3n para configurar.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Seleccione una direcci\u00f3n del dispositivo para eliminar" + }, + "description": "Eliminar una anulaci\u00f3n del dispositivo", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "Seleccione la direcci\u00f3n del dispositivo para eliminar" + }, + "description": "Eliminar un dispositivo X10", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json new file mode 100644 index 00000000000..9922cedb0a3 --- /dev/null +++ b/homeassistant/components/insteon/translations/fr.json @@ -0,0 +1,54 @@ +{ + "config": { + "step": { + "init": { + "title": "Insteon" + } + } + }, + "options": { + "error": { + "select_single": "S\u00e9lectionnez une option" + }, + "step": { + "add_override": { + "title": "Insteon" + }, + "add_x10": { + "data": { + "platform": "Plate-forme" + }, + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "Nouveau nom d\u2019h\u00f4te ou adresse IP", + "password": "Nouveau mot de passe", + "port": "Nouveau num\u00e9ro de port", + "username": "Nouveau nom d\u2019utilisateur" + }, + "title": "Insteon" + }, + "init": { + "data": { + "remove_x10": "Retirez un p\u00e9riph\u00e9rique X10." + }, + "description": "S\u00e9lectionnez une option \u00e0 configurer.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "S\u00e9lectionner une adresse de p\u00e9riph\u00e9rique \u00e0 retirer" + }, + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "S\u00e9lectionner une adresse de p\u00e9riph\u00e9rique \u00e0 retirer" + }, + "description": "Retirer un p\u00e9riph\u00e9rique X10", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/it.json b/homeassistant/components/insteon/translations/it.json new file mode 100644 index 00000000000..4cafb5bf711 --- /dev/null +++ b/homeassistant/components/insteon/translations/it.json @@ -0,0 +1,115 @@ +{ + "config": { + "abort": { + "already_configured": "Una connessione modem Insteon \u00e8 gi\u00e0 configurata", + "cannot_connect": "Impossibile connettersi al modem Insteon" + }, + "error": { + "cannot_connect": "Impossibile connettersi al modem Insteon, riprovare.", + "select_single": "Selezionare un'opzione." + }, + "step": { + "hub1": { + "data": { + "host": "Indirizzo IP dell'hub", + "port": "Porta IP" + }, + "description": "Configurare la versione 1 di Insteon Hub (pre-2014).", + "title": "Insteon Hub versione 1" + }, + "hub2": { + "data": { + "host": "Indirizzo IP dell'hub", + "password": "Password", + "port": "Porta IP", + "username": "Nome utente" + }, + "description": "Configurare la versione 2 di Insteon Hub.", + "title": "Insteon Hub versione 2" + }, + "init": { + "data": { + "hubv1": "Hub versione 1 (precedente al 2014)", + "hubv2": "Hub versione 2", + "plm": "Modem PowerLink (PLM)" + }, + "description": "Selezionare il tipo di modem Insteon.", + "title": "Insteon" + }, + "plm": { + "data": { + "device": "Dispositivo PLM (ad esempio /dev/ttyUSB0 o COM3)" + }, + "description": "Configurare Insteon PowerLink Modem (PLM).", + "title": "Insteon PLM" + } + } + }, + "options": { + "abort": { + "already_configured": "Una connessione modem Insteon \u00e8 gi\u00e0 configurata", + "cannot_connect": "Impossibile connettersi al modem Insteon" + }, + "error": { + "cannot_connect": "Impossibile connettersi al modem Insteon, riprovare.", + "input_error": "Voci non valide, si prega di controllare i valori.", + "select_single": "Selezionare un'opzione." + }, + "step": { + "add_override": { + "data": { + "address": "Indirizzo del dispositivo (ad es. 1a2b3c)", + "cat": "Categoria del dispositivo (ad es. 0x10)", + "subcat": "Sottocategoria del dispositivo (ad es. 0x0a)" + }, + "description": "Aggiungi una sostituzione del dispositivo.", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "Codice casa (a - p)", + "platform": "Piattaforma", + "steps": "Livelli del dimmer (solo per dispositivi luminosi, predefiniti 22)", + "unitcode": "Codice unit\u00e0 (1 - 16)" + }, + "description": "Cambia la password di Insteon Hub.", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "Nuovo nome host o indirizzo IP", + "password": "Nuova password", + "port": "Nuovo numero di porta", + "username": "Nuovo nome utente" + }, + "description": "Modificare le informazioni di connessione di Insteon Hub. \u00c8 necessario riavviare Home Assistant dopo aver apportato questa modifica. Ci\u00f2 non modifica la configurazione dell'Hub stesso. Per modificare la configurazione nell'Hub, utilizzare l'app Hub.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Aggiungi una sostituzione del dispositivo.", + "add_x10": "Aggiungi un dispositivo X10.", + "change_hub_config": "Modificare la configurazione dell'hub.", + "remove_override": "Rimuovere una sostituzione del dispositivo.", + "remove_x10": "Rimuovi un dispositivo X10." + }, + "description": "Selezionare un'opzione da configurare.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Selezionare un indirizzo del dispositivo da rimuovere" + }, + "description": "Rimuovere una sostituzione del dispositivo", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "Selezionare un indirizzo del dispositivo da rimuovere" + }, + "description": "Rimuovi un dispositivo X10", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/lb.json b/homeassistant/components/insteon/translations/lb.json new file mode 100644 index 00000000000..bff6ff757c1 --- /dev/null +++ b/homeassistant/components/insteon/translations/lb.json @@ -0,0 +1,115 @@ +{ + "config": { + "abort": { + "already_configured": "Eng Insteon Modem Verbindung ass scho konfigur\u00e9iert", + "cannot_connect": "Keng Verbindung mam Insteon Modem m\u00e9iglech" + }, + "error": { + "cannot_connect": "Feeler beim verbannen mam Insteon Modem, prob\u00e9ier w.e.g. nach emol.", + "select_single": "Eng Optioun auswielen." + }, + "step": { + "hub1": { + "data": { + "host": "Hub IP Adress", + "port": "IP Port" + }, + "description": "Insteon Hub Versioun 1 (pre-2014) konfigur\u00e9ieren.", + "title": "Insteon Hub Versioun 1" + }, + "hub2": { + "data": { + "host": "Hub IP Adress", + "password": "Passwuert", + "port": "IP Port", + "username": "Benotzernumm" + }, + "description": "Insteon Hub Versioun 2 konfigur\u00e9ieren.", + "title": "Insteon Hub Versioun 2" + }, + "init": { + "data": { + "hubv1": "Hub Versioun 1 (Pre-2014)", + "hubv2": "Hub Versioun 2", + "plm": "Powerlink Modem (PLM)" + }, + "description": "Insteon Modem Typ auswielen.", + "title": "Insteon" + }, + "plm": { + "data": { + "device": "PLM Apparat (Beispill /dev/ttyUSB0 oder COM3)" + }, + "description": "Insteon PowerLink Modem (PLM) konfigur\u00e9ieren.", + "title": "Insteon PLM" + } + } + }, + "options": { + "abort": { + "already_configured": "Eng Insteon Modem Verbindung ass scho konfigur\u00e9iert", + "cannot_connect": "Keng Verbindung mam Insteon Modem m\u00e9iglech" + }, + "error": { + "cannot_connect": "Feeler beim verbannen mam Insteon Modem, prob\u00e9ier w.e.g. nach emol.", + "input_error": "Ong\u00eblteg Entr\u00e9e\u00ebn, iwwerpr\u00e9if deng W\u00e4erter.", + "select_single": "Eng Optioun auswielen." + }, + "step": { + "add_override": { + "data": { + "address": "Apparat Adress (Beispill 1a2b3c)", + "cat": "Apparat Kategorie (Beispill 0x10)", + "subcat": "Apparat \u00cbnnerkategorie (Beispill 0x0a)" + }, + "description": "Apparat iwwerschr\u00e9iwen dob\u00e4isetzen", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "Hauscode (a-p)", + "platform": "Plattform", + "steps": "Dimmer Schr\u00ebtt (n\u00ebmme fir Luuchten, standard 22)", + "unitcode": "Unitcode (1-16)" + }, + "description": "Insteon Hub passwuert \u00e4nneren", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "Neien Host Numm oder IP Adresse", + "password": "Neit Passwuert", + "port": "Nei Port Nummer", + "username": "Neie Benotzernumm" + }, + "description": "Insteon Hub Verbindungs Informatiounen \u00e4nneren. Du muss Home Assistant no der \u00c4nnerung fr\u00ebsch starten. D\u00ebst \u00e4nnert net d'Konfiguratioun vum Hub selwer. Fir d'Konfiguratioun vum Hub ze \u00e4nnere benotz d'Hub App.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Apparat iwwerschr\u00e9iwen dob\u00e4isetzen", + "add_x10": "Een X10 Apparat dob\u00e4isetzen.", + "change_hub_config": "Hub Konfiguratioun \u00e4nneren.", + "remove_override": "Apparat iwwerschr\u00e9iwen l\u00e4schen", + "remove_x10": "Een X10 Apparat l\u00e4schen." + }, + "description": "Eng Optioun auswielen fir ze konfigur\u00e9ieren", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Eng Apparat Adress auswielen fir ze l\u00e4schen" + }, + "description": "Apparat iwwerschr\u00e9iwen l\u00e4schen", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "Wiel eng Adress vun egem Apparat aus fir ze l\u00e4schen" + }, + "description": "Een X10 Apparat l\u00e4schen", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/no.json b/homeassistant/components/insteon/translations/no.json new file mode 100644 index 00000000000..e0ff69823e4 --- /dev/null +++ b/homeassistant/components/insteon/translations/no.json @@ -0,0 +1,115 @@ +{ + "config": { + "abort": { + "already_configured": "En Insteon-modemtilkobling er allerede konfigurert", + "cannot_connect": "Kan ikke koble til Insteon-modemet" + }, + "error": { + "cannot_connect": "Kan ikke koble til Insteon-modemet, pr\u00f8v p\u00e5 nytt.", + "select_single": "Velg ett alternativ." + }, + "step": { + "hub1": { + "data": { + "host": "Hub-IP-adresse", + "port": "IP-port" + }, + "description": "Konfigurer Insteon Hub versjon 1 (f\u00f8r 2014).", + "title": "Insteon Hub versjon 1" + }, + "hub2": { + "data": { + "host": "Hub-IP-adresse", + "password": "Passord", + "port": "IP-port", + "username": "Brukernavn" + }, + "description": "Konfigurer Insteon Hub versjon 2.", + "title": "Insteon Hub versjon 2" + }, + "init": { + "data": { + "hubv1": "Hub versjon 1 (f\u00f8r 2014)", + "hubv2": "Hub versjon 2", + "plm": "PowerLink-modem (PLM)" + }, + "description": "Velg Insteon modemtype.", + "title": "Insteon" + }, + "plm": { + "data": { + "device": "PLM-enhet (dvs. /dev/ttyUSB0 eller COM3)" + }, + "description": "Konfigurer Insteon PowerLink-modem (PLM).", + "title": "Insteon PLM" + } + } + }, + "options": { + "abort": { + "already_configured": "En Insteon-modemtilkobling er allerede konfigurert", + "cannot_connect": "Kan ikke koble til Insteon-modemet" + }, + "error": { + "cannot_connect": "Kan ikke koble til Insteon-modemet, pr\u00f8v p\u00e5 nytt.", + "input_error": "Ugyldige oppf\u00f8ringer, vennligst sjekk verdiene dine.", + "select_single": "Velg ett alternativ." + }, + "step": { + "add_override": { + "data": { + "address": "Enhetsadresse (dvs. 1a2b3c)", + "cat": "Enhetskategori (dvs. 0x10)", + "subcat": "Underkategori for enhet (dvs. 0x0a)" + }, + "description": "Legg til en enhetsoverstyring.", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "Huskode (a - p)", + "platform": "Plattform", + "steps": "Dimmer trinn (kun for lette enheter, standard 22)", + "unitcode": "Enhetskode (1 - 16)" + }, + "description": "Endre insteon hub-passordet.", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "Nytt vertsnavn eller IP-adresse", + "password": "Nytt passord", + "port": "Nytt portnummer", + "username": "Nytt brukernavn" + }, + "description": "Endre Insteon Hub-tilkoblingsinformasjonen. Du m\u00e5 starte Home Assistant p\u00e5 nytt n\u00e5r du har gjort denne endringen. Dette endrer ikke konfigurasjonen av selve huben. For \u00e5 endre konfigurasjonen i huben bruker du hub-appen.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Legg til en enhetsoverstyring.", + "add_x10": "Legg til en X10-enhet.", + "change_hub_config": "Endre hub-konfigurasjonen.", + "remove_override": "Fjern en enhet overstyring.", + "remove_x10": "Fjern en X10-enhet." + }, + "description": "Velg et alternativ for \u00e5 konfigurere.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Velg en enhetsadresse du vil fjerne" + }, + "description": "Fjerne en enhetsoverstyring", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "Velg en enhetsadresse du vil fjerne" + }, + "description": "Fjern en X10-enhet", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/pt-BR.json b/homeassistant/components/insteon/translations/pt-BR.json new file mode 100644 index 00000000000..e14afa3e18f --- /dev/null +++ b/homeassistant/components/insteon/translations/pt-BR.json @@ -0,0 +1,67 @@ +{ + "config": { + "step": { + "hub2": { + "data": { + "host": "Endere\u00e7o IP do hub", + "password": "Senha", + "port": "Porta IP", + "username": "Nome de usu\u00e1rio" + }, + "description": "Configure o Insteon Hub Vers\u00e3o 2.", + "title": "Insteon Hub vers\u00e3o 2" + }, + "init": { + "data": { + "hubv1": "Hub Vers\u00e3o 1 (Pr\u00e9-2014)", + "hubv2": "Hub vers\u00e3o 2" + }, + "description": "Selecione o tipo de modem Insteon." + } + } + }, + "options": { + "abort": { + "already_configured": "Uma conex\u00e3o com modem Insteon j\u00e1 est\u00e1 configurada", + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao modem Insteon." + }, + "error": { + "cannot_connect": "Falha na conex\u00e3o com o modem Insteon, por favor tente novamente.", + "input_error": "Entradas inv\u00e1lidas, por favor, verifique seus valores.", + "select_single": "Selecione uma op\u00e7\u00e3o." + }, + "step": { + "add_override": { + "data": { + "address": "Endere\u00e7o do dispositivo (ou seja, 1a2b3c)", + "cat": "Subcategoria de dispositivo (ou seja, 0x0a)", + "subcat": "Subcategoria de dispositivo (ou seja, 0x0a)" + }, + "description": "Escolha um dispositivo para sobrescrever" + }, + "add_x10": { + "data": { + "platform": "Plataforma", + "steps": "Etapas de dimmer (apenas para dispositivos de lux, padr\u00e3o 22)" + }, + "description": "Altere a senha do Insteon Hub." + }, + "change_hub_config": { + "data": { + "password": "Nova Senha", + "port": "Novo n\u00famero da porta", + "username": "Novo usu\u00e1rio" + } + }, + "init": { + "data": { + "add_x10": "Adicionar um dispositivo X10" + } + }, + "remove_x10": { + "description": "Remover um dispositivo X10", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/ru.json b/homeassistant/components/insteon/translations/ru.json new file mode 100644 index 00000000000..620bcdc9154 --- /dev/null +++ b/homeassistant/components/insteon/translations/ru.json @@ -0,0 +1,115 @@ +{ + "config": { + "abort": { + "already_configured": "\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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437.", + "select_single": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u043f\u0446\u0438\u044e." + }, + "step": { + "hub1": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "port": "IP \u043f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Insteon Hub \u0432\u0435\u0440\u0441\u0438\u0438 1 (\u0434\u043e 2014 \u0433\u043e\u0434\u0430)", + "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0438\u044f 1" + }, + "hub2": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "IP \u043f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Insteon Hub \u0432\u0435\u0440\u0441\u0438\u0438 2", + "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0438\u044f 2" + }, + "init": { + "data": { + "hubv1": "\u0412\u0435\u0440\u0441\u0438\u044f 1 (\u0434\u043e 2014 \u0433\u043e\u0434\u0430)", + "hubv2": "\u0412\u0435\u0440\u0441\u0438\u044f 2", + "plm": "\u041c\u043e\u0434\u0435\u043c PowerLink (PLM)" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0430 Insteon.", + "title": "Insteon" + }, + "plm": { + "data": { + "device": "PLM-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: /dev/ttyUSB0 \u0438\u043b\u0438 COM3)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u043e\u0434\u0435\u043c\u0430 Insteon PowerLink (PLM)", + "title": "Insteon PLM" + } + } + }, + "options": { + "abort": { + "already_configured": "\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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437.", + "input_error": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f.", + "select_single": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u043f\u0446\u0438\u044e." + }, + "step": { + "add_override": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 1a2b3c)", + "cat": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 0x10)", + "subcat": "\u041f\u043e\u0434\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 0x0a)" + }, + "description": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "\u041a\u043e\u0434 \u0434\u043e\u043c\u0430 (a - p)", + "platform": "\u041f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430", + "steps": "\u0428\u0430\u0433 \u0434\u0438\u043c\u043c\u0435\u0440\u0430 (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043e\u0441\u0432\u0435\u0442\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0440\u0438\u0431\u043e\u0440\u043e\u0432, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e 22)", + "unitcode": "\u042e\u043d\u0438\u0442\u043a\u043e\u0434 (1 - 16)" + }, + "description": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u043a Insteon Hub", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "\u041d\u043e\u0432\u043e\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041d\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041d\u043e\u0432\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430", + "username": "\u041d\u043e\u0432\u044b\u0439 \u043b\u043e\u0433\u0438\u043d" + }, + "description": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Insteon Hub. \u041f\u043e\u0441\u043b\u0435 \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Home Assistant. \u042d\u0442\u043e \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0427\u0442\u043e\u0431\u044b \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Hub.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "add_x10": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e X10", + "change_hub_config": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430", + "remove_override": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "remove_x10": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e X10" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u043f\u0446\u0438\u044e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 X10", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json new file mode 100644 index 00000000000..910377da4d2 --- /dev/null +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -0,0 +1,115 @@ +{ + "config": { + "abort": { + "already_configured": "Insteon \u6578\u64da\u6a5f\u9023\u7dda\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Insteon \u6578\u64da\u6a5f" + }, + "error": { + "cannot_connect": "Insteon \u6578\u64da\u6a5f\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "select_single": "\u9078\u64c7\u9078\u9805\u3002" + }, + "step": { + "hub1": { + "data": { + "host": "Hub IP \u4f4d\u5740", + "port": "IP \u57e0" + }, + "description": "\u8a2d\u5b9a Insteon Hub \u7b2c 1 \u7248\uff082014 \u5e74\u4ee5\u524d\uff09\u3002", + "title": "Insteon Hub \u7b2c 1 \u7248" + }, + "hub2": { + "data": { + "host": "Hub IP \u4f4d\u5740", + "password": "\u5bc6\u78bc", + "port": "IP \u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a Insteon Hub \u7b2c 2 \u7248\u3002", + "title": "Insteon Hub \u7b2c 2 \u7248" + }, + "init": { + "data": { + "hubv1": "Insteon Hub \u7b2c 1 \u7248\uff082014 \u5e74\u4ee5\u524d\uff09", + "hubv2": "Insteon Hub \u7b2c 2 \u7248", + "plm": "PowerLink Modem (PLM)" + }, + "description": "\u9078\u64c7 Insteon \u6578\u64da\u6a5f\u985e\u578b\u3002", + "title": "Insteon" + }, + "plm": { + "data": { + "device": "PLM \u8a2d\u5099\uff08\u4f8b\u5982 /dev/ttyUSB0 \u6216 COM3\uff09" + }, + "description": "\u8a2d\u5b9a PowerLink Modem (PLM)\u3002", + "title": "Insteon PLM" + } + } + }, + "options": { + "abort": { + "already_configured": "Insteon \u6578\u64da\u6a5f\u9023\u7dda\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Insteon \u6578\u64da\u6a5f\u3002" + }, + "error": { + "cannot_connect": "Insteon \u6578\u64da\u6a5f\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "input_error": "\u7269\u4ef6\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u503c\u3002", + "select_single": "\u9078\u64c7\u9078\u9805\u3002" + }, + "step": { + "add_override": { + "data": { + "address": "\u8a2d\u5099\u4f4d\u5740\uff08\u4f8b\u5982 1a2b3c\uff09", + "cat": "\u8a2d\u5099\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x10\uff09", + "subcat": "\u8a2d\u5099\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x0a\uff09" + }, + "description": "\u65b0\u589e\u8a2d\u5099\u8986\u5beb\u3002", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "Housecode (a - p)", + "platform": "\u5e73\u53f0", + "steps": "\u8abf\u5149\u968e\u6bb5\uff08\u50c5\u9069\u7528\u7167\u660e\u8a2d\u5099\u3001\u9810\u8a2d\u503c\u70ba 22\uff09", + "unitcode": "Unitcode (1 - 16)" + }, + "description": "\u8b8a\u66f4 Insteon Hub \u5bc6\u78bc\u3002", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "\u65b0\u589e\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "password": "\u65b0\u589e\u5bc6\u78bc", + "port": "\u65b0\u589e\u901a\u8a0a\u57e0\u865f", + "username": "\u65b0\u589e\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8b8a\u66f4 Insteon Hub \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 Hub \u8a2d\u5099\u672c\u8eab\u7684\u8a2d\u5b9a\uff0c\u5982\u6b32\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3001\u5247\u8acb\u4f7f\u7528 Hub app\u3002", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "\u65b0\u589e\u8a2d\u5099\u8986\u5beb\u3002", + "add_x10": "\u65b0\u589e X10 \u8a2d\u5099\u3002", + "change_hub_config": "\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3002", + "remove_override": "\u79fb\u9664\u8a2d\u5099\u8986\u5beb", + "remove_x10": "\u79fb\u9664 X10 \u8a2d\u5099\u3002" + }, + "description": "\u9078\u64c7\u9078\u9805\u4ee5\u8a2d\u5b9a", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "\u9078\u64c7\u8a2d\u5099\u4f4d\u5740\u4ee5\u79fb\u9664" + }, + "description": "\u79fb\u9664\u8a2d\u5099\u8986\u5beb", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "\u9078\u64c7\u8a2d\u5099\u4f4d\u5740\u4ee5\u79fb\u9664" + }, + "description": "\u79fb\u9664 X10 \u8a2d\u5099", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/no.json b/homeassistant/components/ipp/translations/no.json index 4e94efe71c1..749b5e5bab8 100644 --- a/homeassistant/components/ipp/translations/no.json +++ b/homeassistant/components/ipp/translations/no.json @@ -19,7 +19,6 @@ "data": { "base_path": "Relativ bane til skriveren", "host": "Vert", - "port": "", "ssl": "Skriveren st\u00f8tter kommunikasjon over SSL/TLS", "verify_ssl": "Skriveren bruker et riktig SSL-sertifikat" }, diff --git a/homeassistant/components/ipp/translations/pt-BR.json b/homeassistant/components/ipp/translations/pt-BR.json index f992520501e..704cc017a9b 100644 --- a/homeassistant/components/ipp/translations/pt-BR.json +++ b/homeassistant/components/ipp/translations/pt-BR.json @@ -14,6 +14,7 @@ "user": { "data": { "base_path": "Caminho relativo para a impressora", + "port": "Porta", "ssl": "A impressora suporta comunica\u00e7\u00e3o via SSL/TLS", "verify_ssl": "A impressora usa um certificado SSL adequado" }, diff --git a/homeassistant/components/ipp/translations/ru.json b/homeassistant/components/ipp/translations/ru.json index 6fdfd333773..4953c9dae5c 100644 --- a/homeassistant/components/ipp/translations/ru.json +++ b/homeassistant/components/ipp/translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\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.", - "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443 \u0438\u0437-\u0437\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f.", "ipp_error": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 IPP.", "ipp_version_error": "\u0412\u0435\u0440\u0441\u0438\u044f IPP \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c.", @@ -10,7 +10,7 @@ "unique_id_required": "\u041d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430\u044f \u0434\u043b\u044f \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f." }, "error": { - "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 SSL/TLS." }, "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440: {name}", diff --git a/homeassistant/components/iqvia/translations/ca.json b/homeassistant/components/iqvia/translations/ca.json index bfc1cf6b452..68774277482 100644 --- a/homeassistant/components/iqvia/translations/ca.json +++ b/homeassistant/components/iqvia/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Aquest codi postal ja s\u2019ha configurat." + "already_configured": "Aquest codi postal ja est\u00e0 configurat." }, "error": { "invalid_zip_code": "Codi postal incorrecte" diff --git a/homeassistant/components/iqvia/translations/it.json b/homeassistant/components/iqvia/translations/it.json index 599974a8d26..fb88883eb0f 100644 --- a/homeassistant/components/iqvia/translations/it.json +++ b/homeassistant/components/iqvia/translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Questo codice di avviamento postale \u00e8 gi\u00e0 stato configurato." + }, "error": { "invalid_zip_code": "Il CAP non \u00e8 valido" }, diff --git a/homeassistant/components/iqvia/translations/lb.json b/homeassistant/components/iqvia/translations/lb.json index 4941358ae08..54a6bbce24d 100644 --- a/homeassistant/components/iqvia/translations/lb.json +++ b/homeassistant/components/iqvia/translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "D\u00ebs Postleitzuel ass scho konfigur\u00e9iert." + }, "error": { "invalid_zip_code": "Postleitzuel ass ong\u00eblteg" }, diff --git a/homeassistant/components/iqvia/translations/no.json b/homeassistant/components/iqvia/translations/no.json index 98526dfed24..9359014dcf1 100644 --- a/homeassistant/components/iqvia/translations/no.json +++ b/homeassistant/components/iqvia/translations/no.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Postnummer" }, - "description": "Fyll ut ditt amerikanske eller kanadiske postnummer.", - "title": "" + "description": "Fyll ut ditt amerikanske eller kanadiske postnummer." } } } diff --git a/homeassistant/components/iqvia/translations/zh-Hant.json b/homeassistant/components/iqvia/translations/zh-Hant.json index 68b19b69d21..f6a71960c0a 100644 --- a/homeassistant/components/iqvia/translations/zh-Hant.json +++ b/homeassistant/components/iqvia/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u90f5\u905e\u5340\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, "error": { "invalid_zip_code": "\u90f5\u905e\u5340\u865f\u7121\u6548" }, diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json index 35e52de882f..a39fc58dc25 100644 --- a/homeassistant/components/isy994/translations/nl.json +++ b/homeassistant/components/isy994/translations/nl.json @@ -1,5 +1,5 @@ { "config": { - "flow_title": "Universele apparaten ISY994 {naam} ({host})" + "flow_title": "Universele apparaten ISY994 {name} ({host})" } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/pt-BR.json b/homeassistant/components/isy994/translations/pt-BR.json index 65a087f8789..090e9a21530 100644 --- a/homeassistant/components/isy994/translations/pt-BR.json +++ b/homeassistant/components/isy994/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "error": { - "invalid_host": "A entrada do host n\u00e3o est\u00e1 no formato de URL completo, por exemplo, http://192.168.10.100:80" + "invalid_host": "A entrada do host n\u00e3o est\u00e1 no formato de URL completo, por exemplo, http://192.168.10.100:80", + "unknown": "Erro inesperado." }, "flow_title": "Dispositivos universais ISY994 {name} ({host})", "step": { diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index ed34d79969f..b6222417295 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "invalid_host": "URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/kodi/translations/ca.json b/homeassistant/components/kodi/translations/ca.json new file mode 100644 index 00000000000..d9993e01f54 --- /dev/null +++ b/homeassistant/components/kodi/translations/ca.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix el teu nom d'usuari i contrasenya de Kodi. Els pots trobar a Sistema/Configuraci\u00f3/Xarxa/Serveis." + }, + "discovery_confirm": { + "description": "Vols afegir Kodi (`{name}`) a Home Assistant?", + "title": "Kodi descobert" + }, + "host": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "ssl": "Connexi\u00f3 mitjan\u00e7ant SSL" + }, + "description": "Informaci\u00f3 de connexi\u00f3 de Kodi. Assegura't d'activar \"Permet el control de Kodi via HTTP\" a Sistema/Configuraci\u00f3/Xarxa/Serveis." + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "ssl": "Connexi\u00f3 mitjan\u00e7ant SSL" + }, + "description": "Informaci\u00f3 de connexi\u00f3 de Kodi. Assegura't d'activar \"Permet el control de Kodi via HTTP\" a Sistema/Configuraci\u00f3/Xarxa/Serveis." + }, + "ws_port": { + "data": { + "ws_port": "Port" + }, + "description": "Port del WebSocket (a Kodi pot anomenar-se TCP). Per connectar-se a trav\u00e9s de WebSocket has d'activar \"Permet que programes... controlin Kodi\" a Sistema/Configuraci\u00f3/Xarxa/Serveis. Si el WebSocket no est\u00e0 activat, elimina el port i deixa-ho buit." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "S'ha sol\u00b7licitat la desactivaci\u00f3 de {entity_name}", + "turn_on": "S'ha sol\u00b7licitat l'activaci\u00f3 de {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/en.json b/homeassistant/components/kodi/translations/en.json new file mode 100644 index 00000000000..4a9a48ea9ca --- /dev/null +++ b/homeassistant/components/kodi/translations/en.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Please enter your Kodi user name and password. These can be found in System/Settings/Network/Services." + }, + "discovery_confirm": { + "description": "Do you want to add Kodi (`{name}`) to Home Assistant?", + "title": "Discovered Kodi" + }, + "host": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Connect over SSL" + }, + "description": "Kodi connection information. Please make sure to enable \"Allow control of Kodi via HTTP\" in System/Settings/Network/Services." + }, + "user": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Connect over SSL" + }, + "description": "Kodi connection information. Please make sure to enable \"Allow control of Kodi via HTTP\" in System/Settings/Network/Services." + }, + "ws_port": { + "data": { + "ws_port": "Port" + }, + "description": "The WebSocket port (sometimes called TCP port in Kodi). In order to connect over WebSocket, you need to enable \"Allow programs ... to control Kodi\" in System/Settings/Network/Services. If WebSocket is not enabled, remove the port and leave empty." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} was requested to turn off", + "turn_on": "{entity_name} was requested to turn on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json new file mode 100644 index 00000000000..aabc87cb41d --- /dev/null +++ b/homeassistant/components/kodi/translations/es.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autentificacion invalida", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Por favor, introduzca su nombre de usuario y contrase\u00f1a de Kodi. Estos se pueden encontrar en Sistema/Configuraci\u00f3n/Red/Servicios." + }, + "discovery_confirm": { + "description": "\u00bfQuieres agregar Kodi (`{name}`) a Home Assistant?", + "title": "Descubierto Kodi" + }, + "host": { + "data": { + "host": "Host", + "port": "Puerto", + "ssl": "Conectar a trav\u00e9s de SSL" + }, + "description": "Informaci\u00f3n de la conexi\u00f3n de Kodi. Por favor, aseg\u00farese de habilitar \"Permitir el control de Kodi v\u00eda HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto", + "ssl": "Conectar a trav\u00e9s de SSL" + }, + "description": "Informaci\u00f3n de la conexi\u00f3n de Kodi. Por favor, aseg\u00farese de habilitar \"Permitir el control de Kodi v\u00eda HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." + }, + "ws_port": { + "data": { + "ws_port": "Puerto" + }, + "description": "El puerto WebSocket (a veces llamado puerto TCP en Kodi). Para conectarse a trav\u00e9s de WebSocket, necesitas habilitar \"Permitir a los programas... controlar Kodi\" en Sistema/Configuraci\u00f3n/Red/Servicios. Si el WebSocket no est\u00e1 habilitado, elimine el puerto y d\u00e9jelo vac\u00edo." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "Se solicit\u00f3 que {entity_name} se apagara", + "turn_on": "Se solicit\u00f3 que {entity_name} se encendiera" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/fr.json b/homeassistant/components/kodi/translations/fr.json new file mode 100644 index 00000000000..5b3c0e15021 --- /dev/null +++ b/homeassistant/components/kodi/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "invalid_auth": "Authentification erron\u00e9e", + "unknown": "Erreur inattendue" + }, + "flow_title": "Kodi: {name}", + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port", + "ssl": "Connexion via SSL" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 {entity_name} de s'\u00e9teindre", + "turn_on": "Il a \u00e9t\u00e9 demand\u00e9 \u00e0 {entity_name} de s'allumer" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/it.json b/homeassistant/components/kodi/translations/it.json new file mode 100644 index 00000000000..a48d8ca9bb0 --- /dev/null +++ b/homeassistant/components/kodi/translations/it.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci il tuo nome utente e la password Kodi. Questi sono disponibili in Sistema/Impostazioni/Rete/Servizi." + }, + "discovery_confirm": { + "description": "Vuoi aggiungere Kodi (`{name}`) a Home Assistant?", + "title": "Rilevato Kodi" + }, + "host": { + "data": { + "host": "Host", + "port": "Porta", + "ssl": "Connettiti tramite SSL" + }, + "description": "Informazioni sulla connessione Kodi. Assicurati di abilitare \"Consenti il controllo di Kodi tramite HTTP\" in Sistema/Impostazioni/Rete/Servizi." + }, + "user": { + "data": { + "host": "Host", + "port": "Porta", + "ssl": "Connettiti tramite SSL" + }, + "description": "Informazioni sulla connessione Kodi. Assicurati di abilitare \"Consenti il controllo di Kodi tramite HTTP\" in Sistema/Impostazioni/Rete/Servizi." + }, + "ws_port": { + "data": { + "ws_port": "Porta" + }, + "description": "La porta WebSocket (a volte chiamata porta TCP in Kodi). Per connetterti tramite WebSocket, devi abilitare \"Consenti ai programmi ... di controllare Kodi\" in Sistema/Impostazioni/Rete/Servizi. Se WebSocket non \u00e8 abilitato, rimuovere la porta e lasciare vuoto." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "A {entity_name} \u00e8 stato richiesto di spegnere", + "turn_on": "A {entity_name} \u00e8 stato richiesto di accendere" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/lb.json b/homeassistant/components/kodi/translations/lb.json new file mode 100644 index 00000000000..872615f5bea --- /dev/null +++ b/homeassistant/components/kodi/translations/lb.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "description": "G\u00ebff d\u00e4in Kodi Benotzernumm a Passwuert an. D\u00e9i stinn am System/Settings/Network/Services." + }, + "discovery_confirm": { + "description": "Soll Kodi (`{name}`) am Home Assistant dob\u00e4i gesaat ginn?", + "title": "Kodi entdeckt" + }, + "host": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Iwwer SSL verbannen" + }, + "description": "Kodi Verbindungs Informatiounen. Stell s\u00e9cher dass d'Optioun \"Allow control of Kodi via HTTP\" aktiv ass an System/Settings/Network/Services." + }, + "user": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Iwwer SSL verbannen" + }, + "description": "Kodi Verbindungs Informatiounen. Stell s\u00e9cher dass d'Optioun \"Allow control of Kodi via HTTP\" aktiv ass an System/Settings/Network/Services." + }, + "ws_port": { + "data": { + "ws_port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/no.json b/homeassistant/components/kodi/translations/no.json new file mode 100644 index 00000000000..1c8bc3d6a53 --- /dev/null +++ b/homeassistant/components/kodi/translations/no.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes.", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes.", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Vennligst skriv inn ditt Kodi brukernavn og passord. Disse finner du i System/Innstillinger/Nettverk/Tjenester." + }, + "discovery_confirm": { + "description": "Vil du legge til Kodi ({name}) i Home Assistant?", + "title": "Oppdaget Kodi" + }, + "host": { + "data": { + "host": "Vert", + "port": "Port", + "ssl": "Koble til via SSL" + }, + "description": "Kodi-tilkoblingsinformasjon. Vennligst s\u00f8rg for \u00e5 aktivere \"Tillat kontroll av Kodi via HTTP\" i System / Innstillinger / Nettverk / Tjenester." + }, + "user": { + "data": { + "host": "Vert", + "port": "Port", + "ssl": "Koble til via SSL" + }, + "description": "Kodi-tilkoblingsinformasjon. Vennligst s\u00f8rg for \u00e5 aktivere \"Tillat kontroll av Kodi via HTTP\" i System / Innstillinger / Nettverk / Tjenester." + }, + "ws_port": { + "data": { + "ws_port": "Port" + }, + "description": "WebSocket-porten (noen ganger kalt TCP-port i Kodi). For \u00e5 koble til over WebSocket, m\u00e5 du aktivere \"Tillat programmer ... for \u00e5 kontrollere Kodi\" i System/Innstillinger/Nettverk/Tjenester. Hvis WebSocket ikke er aktivert, fjerner du porten og lar den st\u00e5 tom." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} ble bedt om \u00e5 sl\u00e5 av", + "turn_on": "{entity_name} ble bedt om \u00e5 sl\u00e5 p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json new file mode 100644 index 00000000000..8702c016871 --- /dev/null +++ b/homeassistant/components/kodi/translations/ru.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c Kodi. \u0418\u0445 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438, \u043f\u0435\u0440\u0435\u0439\u0434\u044f \u0432 \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\" - \"\u0421\u043b\u0443\u0436\u0431\u044b\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\"." + }, + "discovery_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Kodi (`{name}`)?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 Kodi" + }, + "host": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u043e SSL" + }, + "description": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e HTTP\" \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\" - \"\u0421\u043b\u0443\u0436\u0431\u044b\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\"." + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u043e SSL" + }, + "description": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u043e\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e HTTP\" \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\" - \"\u0421\u043b\u0443\u0436\u0431\u044b\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\"." + }, + "ws_port": { + "data": { + "ws_port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e WebSocket. \u0427\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 WebSocket, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f\u043c\u0438 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \"\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\" - \"\u0421\u043b\u0443\u0436\u0431\u044b\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435\". \u0415\u0441\u043b\u0438 WebSocket \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}", + "turn_on": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json new file mode 100644 index 00000000000..b00f6245366 --- /dev/null +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Kodi\uff1a{name}", + "step": { + "credentials": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u8f38\u5165 Kodi \u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\uff0c\u53ef\u4ee5\u65bc\u300c\u7cfb\u7d71/\u8a2d\u5b9a/\u7db2\u8def/\u670d\u52d9\u300d\u4e2d\u627e\u5230\u3002" + }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u65b0\u589e Kodi (`{name}`) \u81f3 Home Assistant\uff1f", + "title": "\u5df2\u641c\u7d22\u5230\u7684 Kodi" + }, + "host": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u901a\u904e SSL \u9023\u7dda" + }, + "description": "Kodi \u9023\u7dda\u8cc7\u8a0a\uff0c\u8acb\u78ba\u5b9a\u5df2\u65bc\u300c\u7cfb\u7d71/\u8a2d\u5b9a/\u7db2\u8def/\u670d\u52d9\u300d\u4e2d\u958b\u555f \"\u5141\u8a31\u900f\u904e HTTP \u63a7\u5236 Kodi\"\u3002" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u901a\u904e SSL \u9023\u7dda" + }, + "description": "Kodi \u9023\u7dda\u8cc7\u8a0a\uff0c\u8acb\u78ba\u5b9a\u5df2\u65bc\u300c\u7cfb\u7d71/\u8a2d\u5b9a/\u7db2\u8def/\u670d\u52d9\u300d\u4e2d\u958b\u555f \"\u5141\u8a31\u900f\u904e HTTP \u63a7\u5236 Kodi\"\u3002" + }, + "ws_port": { + "data": { + "ws_port": "\u901a\u8a0a\u57e0" + }, + "description": "WebSocket \u901a\u8a0a\u57e0\uff08Kodi \u6709\u6642\u5019\u7a31\u4e4b\u70ba TCP \u57e0\uff09\u3002\u6b32\u900f\u904e WebSocket \u9032\u884c\u9023\u7dda\uff0c\u5fc5\u9808\u5148\u65bc\u300c\u7cfb\u7d71/\u8a2d\u5b9a/\u7db2\u8def/\u670d\u52d9\u300d\u4e2d\u958b\u555f \"\u5141\u8a31\u7a0b\u5f0f ... \u63a7\u5236 Kodi\"\u3002\u5047\u5982 WebSocket \u672a\u958b\u555f\uff0c\u8acb\u79fb\u9664\u901a\u8a0a\u57e0\u8f38\u5165\u3001\u4fdd\u6301\u7a7a\u767d\u3002" + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} \u4f9d\u9700\u6c42\u95dc\u9589", + "turn_on": "{entity_name} \u4f9d\u9700\u6c42\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ca.json b/homeassistant/components/konnected/translations/ca.json index 86ef5ab5fc6..8938db2621b 100644 --- a/homeassistant/components/konnected/translations/ca.json +++ b/homeassistant/components/konnected/translations/ca.json @@ -41,7 +41,7 @@ "name": "Nom (opcional)", "type": "Tipus de sensor binari" }, - "description": "Selecciona les opcions pel sensor binari de {zone}", + "description": "Opcions de {zone}", "title": "Configuraci\u00f3 de sensor binari" }, "options_digital": { @@ -50,7 +50,7 @@ "poll_interval": "Interval de sondeig (minuts) (opcional)", "type": "Tipus de sensor" }, - "description": "Selecciona les opcions pel sensor digital de {zone}", + "description": "Opcions de {zone}", "title": "Configuraci\u00f3 de sensor digital" }, "options_io": { @@ -85,6 +85,7 @@ "data": { "api_host": "Substitueix l'URL d'amfitri\u00f3 d'API (opcional)", "blink": "Parpelleja el LED del panell quan s'envien canvis d'estat", + "discovery": "Respon a peticions de descobriment a la teva xarxa", "override_api_host": "Substitueix l'URL per defecte del panell d'amfitri\u00f3 de l'API de Home Assistant" }, "description": "Selecciona el comportament desitjat del panell", @@ -99,7 +100,7 @@ "pause": "Pausa entre polsos (ms) (opcional)", "repeat": "Repeticions (-1 = infinit) (opcional)" }, - "description": "Selecciona les opcions de sortida per a {zone}: estat {state}", + "description": "Opcions de {zone}: estat {state}", "title": "Configuraci\u00f3 de sortida commutable" } } diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index f27fe036fa7..c06248e5158 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -85,6 +85,7 @@ "data": { "api_host": "Invalidar la direcci\u00f3n URL del host de la API (opcional)", "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado", + "discovery": "Responde a las solicitudes de descubrimiento en tu red", "override_api_host": "Reemplazar la URL predeterminada del panel host de la API de Home Assistant" }, "description": "Seleccione el comportamiento deseado para su panel", diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index ab09eb76895..d4fcfdf6500 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -83,7 +83,8 @@ }, "options_misc": { "data": { - "blink": "Voyant du panneau clignotant lors de l'envoi d'un changement d'\u00e9tat" + "blink": "Voyant du panneau clignotant lors de l'envoi d'un changement d'\u00e9tat", + "discovery": "R\u00e9pondre aux demandes de d\u00e9couverte sur votre r\u00e9seau" }, "description": "Veuillez s\u00e9lectionner le comportement souhat\u00e9 de votre panneau", "title": "Configurer divers" diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index bdef475dc6e..ff8b6da9e3d 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -43,7 +43,7 @@ "name": "Nome (opzionale)", "type": "Tipo di sensore binario" }, - "description": "Si prega di selezionare le opzioni per il sensore binario collegato alla {zone}", + "description": "Opzioni {zone}", "title": "Configurare il Sensore Binario" }, "options_digital": { @@ -52,7 +52,7 @@ "poll_interval": "Intervallo di sondaggio (minuti) (opzionale)", "type": "Tipo di sensore" }, - "description": "Si prega di selezionare le opzioni per il sensore digitale collegato alla {zone}", + "description": "Opzioni {zone}", "title": "Configurare il Sensore Digitale" }, "options_io": { @@ -87,6 +87,7 @@ "data": { "api_host": "Sovrascrivi l'URL dell'host API (opzionale)", "blink": "Attiva il lampeggio del LED del pannello durante l'invio del cambiamento di stato ", + "discovery": "Rispondi alle richieste di rilevamento sulla tua rete", "override_api_host": "Sovrascrivi l'URL predefinito del pannello host API di Home Assistant" }, "description": "Seleziona il comportamento desiderato per il tuo pannello", @@ -101,7 +102,7 @@ "pause": "Pausa tra gli impulsi (ms) (opzionale)", "repeat": "Numero di volte da ripetere (-1 = infinito) (opzionale)" }, - "description": "Selezionare le opzioni di uscita per {zone}: stato {state}", + "description": "Opzioni {zone}: stato {state}", "title": "Configurare l'uscita commutabile" } } diff --git a/homeassistant/components/konnected/translations/ko.json b/homeassistant/components/konnected/translations/ko.json index 493ec865bcd..d8d2b70d909 100644 --- a/homeassistant/components/konnected/translations/ko.json +++ b/homeassistant/components/konnected/translations/ko.json @@ -41,7 +41,7 @@ "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", "type": "\uc774\uc9c4 \uc13c\uc11c \uc720\ud615" }, - "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \uc774\uc9c4 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "description": "{zone} \uc635\uc158", "title": "\uc774\uc9c4 \uc13c\uc11c \uad6c\uc131\ud558\uae30" }, "options_digital": { @@ -50,7 +50,7 @@ "poll_interval": "\ud3f4\ub9c1 \uac04\uaca9 (\ubd84) (\uc120\ud0dd \uc0ac\ud56d)", "type": "\uc13c\uc11c \uc720\ud615" }, - "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \ub514\uc9c0\ud138 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "description": "{zone} \uc635\uc158", "title": "\ub514\uc9c0\ud138 \uc13c\uc11c \uad6c\uc131\ud558\uae30" }, "options_io": { @@ -85,6 +85,7 @@ "data": { "api_host": "API \ud638\uc2a4\ud2b8 URL \uc7ac\uc815\uc758 (\uc120\ud0dd \uc0ac\ud56d)", "blink": "\uc0c1\ud0dc \ubcc0\uacbd\uc744 \ubcf4\ub0bc \ub54c \uae5c\ubc15\uc784 \ud328\ub110 LED \ub97c \ucf2d\ub2c8\ub2e4", + "discovery": "\ub124\ud2b8\uc6cc\ud06c\uc758 \uac80\uc0c9 \uc694\uccad\uc5d0 \uc751\ub2f5", "override_api_host": "\uae30\ubcf8 Home Assistant API \ud638\uc2a4\ud2b8 \ud328\ub110 URL \uc7ac\uc815\uc758" }, "description": "\ud328\ub110\uc5d0 \uc6d0\ud558\ub294 \ub3d9\uc791\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", @@ -99,7 +100,7 @@ "pause": "\ud384\uc2a4 \uac04 \uc77c\uc2dc\uc815\uc9c0 \uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", "repeat": "\ubc18\ubcf5 \uc2dc\uac04 (-1 = \ubb34\ud55c) (\uc120\ud0dd \uc0ac\ud56d)" }, - "description": "{zone} \uc5d0 \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694: \uc0c1\ud0dc {state}", + "description": "{zone} \uc635\uc158: {state} \uc0c1\ud0dc", "title": "\uc2a4\uc704\uce58 \ucd9c\ub825 \uad6c\uc131\ud558\uae30" } } diff --git a/homeassistant/components/konnected/translations/lb.json b/homeassistant/components/konnected/translations/lb.json index 8d0272a1ab5..d90602c4005 100644 --- a/homeassistant/components/konnected/translations/lb.json +++ b/homeassistant/components/konnected/translations/lb.json @@ -87,6 +87,7 @@ "data": { "api_host": "API Host URL iwwerschr\u00e9iwen (optionell)", "blink": "Blink panel LED un wann Status \u00c4nnerung gesch\u00e9ckt g\u00ebtt", + "discovery": "\u00c4ntwert op Entdeckungsufroen an dengem Netzwierk", "override_api_host": "Standard Home Assistant API Host Tableau URL iwwerschr\u00e9iwen" }, "description": "Wielt w.e.g. dat gew\u00ebnschte Verhalen fir \u00c4re Panel aus", diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json index 6bd35c0e401..ab7bae93b13 100644 --- a/homeassistant/components/konnected/translations/no.json +++ b/homeassistant/components/konnected/translations/no.json @@ -20,8 +20,7 @@ }, "user": { "data": { - "host": "IP adresse", - "port": "" + "host": "IP adresse" }, "description": "Vennligst skriv inn verten informasjon for din Konnected Panel." } @@ -41,7 +40,7 @@ "name": "Navn (valgfritt)", "type": "Bin\u00e6r sensortype" }, - "description": "Vennligst velg alternativer for bin\u00e6re sensor koblet til {sone}", + "description": "{zone} -alternativer", "title": "Konfigurer bin\u00e6r sensor" }, "options_digital": { @@ -50,7 +49,7 @@ "poll_interval": "Avstemningsintervall (minutter) (valgfritt)", "type": "Sensortype" }, - "description": "Vennligst velg alternativene for den digitale sensor som er koblet til {sone}", + "description": "{zone} -alternativer", "title": "Konfigurere Digital Sensor" }, "options_io": { @@ -85,6 +84,7 @@ "data": { "api_host": "Overstyre API-vert-URL (valgfritt)", "blink": "Blink p\u00e5 LED-lampen n\u00e5r du sender statusendring", + "discovery": "Svar p\u00e5 oppdagelsesforesp\u00f8rsler i nettverket ditt", "override_api_host": "Overstyre standard Home Assistant API-vertspanel-URL" }, "description": "Vennligst velg \u00f8nsket atferd for din panel", @@ -99,7 +99,7 @@ "pause": "Pause mellom pulser (ms) (valgfritt)", "repeat": "Tider \u00e5 gjenta (-1 = uendelig) (valgfritt)" }, - "description": "Velg outputalternativer for {zone} : state {state}", + "description": "{zone} alternativer: tilstand {state}", "title": "Konfigurer utskiftbar utgang" } } diff --git a/homeassistant/components/konnected/translations/ru.json b/homeassistant/components/konnected/translations/ru.json index d3c2816963c..433d6bad4c9 100644 --- a/homeassistant/components/konnected/translations/ru.json +++ b/homeassistant/components/konnected/translations/ru.json @@ -41,7 +41,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "type": "\u0422\u0438\u043f \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430" }, - "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a {zone}.", + "description": "\u041e\u043f\u0446\u0438\u0438 {zone}", "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430" }, "options_digital": { @@ -50,7 +50,7 @@ "poll_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 \u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "type": "\u0422\u0438\u043f \u0441\u0435\u043d\u0441\u043e\u0440\u0430" }, - "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u0435\u043d\u0441\u043e\u0440\u0430, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a {zone}.", + "description": "\u041e\u043f\u0446\u0438\u0438 {zone}", "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u0435\u043d\u0441\u043e\u0440\u0430" }, "options_io": { @@ -85,6 +85,7 @@ "data": { "api_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c URL \u0445\u043e\u0441\u0442\u0430 API (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "blink": "LED-\u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "discovery": "\u041e\u0442\u0432\u0435\u0447\u0430\u0442\u044c \u043d\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438", "override_api_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442-\u043f\u0430\u043d\u0435\u043b\u0438 Home Assistant API" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0436\u0435\u043b\u0430\u0435\u043c\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u043f\u0430\u043d\u0435\u043b\u0438.", @@ -99,7 +100,7 @@ "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0435\u0436\u0434\u0443 \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "repeat": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0439 (-1 = \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u044b\u0445\u043e\u0434\u0430 \u0434\u043b\u044f {zone}: \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 {state}.", + "description": "{zone}: \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 {state}", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u043e\u0433\u043e \u0432\u044b\u0445\u043e\u0434\u0430" } } diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index 45d57eee3c4..c2c63e20c38 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -41,7 +41,7 @@ "name": "\u540d\u7a31\uff08\u9078\u9805\uff09", "type": "\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u985e\u578b" }, - "description": "\u8acb\u9078\u64c7\u6b78\u7d0d\u70ba {zone}\u7684\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u8f38\u51fa\u9078\u9805", + "description": "{zone}\u9078\u9805", "title": "\u8a2d\u5b9a\u4e8c\u9032\u4f4d\u611f\u61c9\u5668" }, "options_digital": { @@ -50,7 +50,7 @@ "poll_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u5206\u9418\uff09\uff08\u9078\u9805\uff09", "type": "\u611f\u61c9\u5668\u985e\u578b" }, - "description": "\u8acb\u9078\u64c7\u6b78\u7d0d\u70ba {zone}\u7684\u6578\u4f4d\u611f\u61c9\u5668\u8f38\u51fa\u9078\u9805", + "description": "{zone}\u9078\u9805", "title": "\u8a2d\u5b9a\u6578\u4f4d\u611f\u61c9\u5668" }, "options_io": { @@ -85,6 +85,7 @@ "data": { "api_host": "\u8986\u5beb API \u4e3b\u6a5f\u7aef URL\uff08\u9078\u9805\uff09", "blink": "\u7576\u50b3\u9001\u72c0\u614b\u8b8a\u66f4\u6642\u3001\u9583\u720d\u9762\u677f LED", + "discovery": "\u56de\u61c9\u7db2\u8def\u4e0a\u7684\u63a2\u7d22\u8acb\u6c42", "override_api_host": "\u8986\u5beb\u9810\u8a2d Home Assistant API \u4e3b\u6a5f\u7aef\u9762\u677f URL" }, "description": "\u8acb\u9078\u64c7\u9762\u677f\u671f\u671b\u884c\u70ba", @@ -99,7 +100,7 @@ "pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09\uff08\u9078\u9805\uff09", "repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09\uff08\u9078\u9805\uff09" }, - "description": "\u8acb\u9078\u64c7 {zone}\u8f38\u51fa\u9078\u9805\uff1a\u72c0\u614b {state}", + "description": "{zone}\u9078\u9805\uff1a\u72c0\u614b {state}", "title": "\u8a2d\u5b9a Switchable \u8f38\u51fa" } } diff --git a/homeassistant/components/life360/translations/pt-BR.json b/homeassistant/components/life360/translations/pt-BR.json index 9136fa3f115..8fba3dce95d 100644 --- a/homeassistant/components/life360/translations/pt-BR.json +++ b/homeassistant/components/life360/translations/pt-BR.json @@ -16,7 +16,7 @@ "user": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" }, "description": "Para definir op\u00e7\u00f5es avan\u00e7adas, consulte [Documenta\u00e7\u00e3o da Life360] ({docs_url}). \n Voc\u00ea pode querer fazer isso antes de adicionar contas.", "title": "Informa\u00e7\u00f5es da conta Life360" diff --git a/homeassistant/components/lovelace/translations/nb.json b/homeassistant/components/lovelace/translations/nb.json deleted file mode 100644 index d8a4c453015..00000000000 --- a/homeassistant/components/lovelace/translations/nb.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "" -} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/no.json b/homeassistant/components/lovelace/translations/no.json deleted file mode 100644 index d8a4c453015..00000000000 --- a/homeassistant/components/lovelace/translations/no.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "" -} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/no.json b/homeassistant/components/luftdaten/translations/no.json index 8c1b69bed07..841ba4ad3da 100644 --- a/homeassistant/components/luftdaten/translations/no.json +++ b/homeassistant/components/luftdaten/translations/no.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Vis p\u00e5 kart", "station_id": "Luftdaten Sensor ID" - }, - "title": "" + } } } } diff --git a/homeassistant/components/met/translations/no.json b/homeassistant/components/met/translations/no.json index 90489288b62..39c8336e074 100644 --- a/homeassistant/components/met/translations/no.json +++ b/homeassistant/components/met/translations/no.json @@ -11,7 +11,6 @@ "longitude": "Lengdegrad", "name": "Navn" }, - "description": "", "title": "Lokasjon" } } diff --git a/homeassistant/components/met/translations/pt.json b/homeassistant/components/met/translations/pt.json index 1afabe51e79..d134c28020f 100644 --- a/homeassistant/components/met/translations/pt.json +++ b/homeassistant/components/met/translations/pt.json @@ -11,7 +11,6 @@ "longitude": "Longitude", "name": "Nome" }, - "description": "", "title": "Localiza\u00e7\u00e3o" } } diff --git a/homeassistant/components/meteo_france/translations/ca.json b/homeassistant/components/meteo_france/translations/ca.json index 4147a4169b1..81f2e6f2d20 100644 --- a/homeassistant/components/meteo_france/translations/ca.json +++ b/homeassistant/components/meteo_france/translations/ca.json @@ -5,14 +5,14 @@ "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard" }, "error": { - "empty": "No hi ha cap resultat de la cerca: comproveu el camp de la ciutat" + "empty": "No s'ha trobat cap resultat en la cerca de la ciutat: comprova el camp ciutat" }, "step": { "cities": { "data": { "city": "Ciutat" }, - "description": "Tria la teva ciutat de la llista", + "description": "Tria una ciutat de la llista", "title": "M\u00e9t\u00e9o-France" }, "user": { diff --git a/homeassistant/components/meteo_france/translations/lb.json b/homeassistant/components/meteo_france/translations/lb.json index 1ec89b44d9f..435eadd57fe 100644 --- a/homeassistant/components/meteo_france/translations/lb.json +++ b/homeassistant/components/meteo_france/translations/lb.json @@ -4,7 +4,17 @@ "already_configured": "Stad ass scho konfigur\u00e9iert", "unknown": "Onbekannte Feeler: prob\u00e9iert sp\u00e9ider nach emol" }, + "error": { + "empty": "Kee Resultat an der Stad Sich: kuckt w.e.g. d'Stad feld" + }, "step": { + "cities": { + "data": { + "city": "Stad" + }, + "description": "Wiel deng Stad aus der L\u00ebscht aus", + "title": "M\u00e9t\u00e9o-France" + }, "user": { "data": { "city": "Stad" @@ -13,5 +23,14 @@ "title": "M\u00e9t\u00e9o-France" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Previsiouns Modus" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/no.json b/homeassistant/components/meteo_france/translations/no.json index 91eea1fcec7..10c5915fdbc 100644 --- a/homeassistant/components/meteo_france/translations/no.json +++ b/homeassistant/components/meteo_france/translations/no.json @@ -19,8 +19,7 @@ "data": { "city": "By" }, - "description": "Fyll inn postnummeret (bare for Frankrike, anbefalt) eller bynavn", - "title": "" + "description": "Fyll inn postnummeret (bare for Frankrike, anbefalt) eller bynavn" } } }, diff --git a/homeassistant/components/meteo_france/translations/pt.json b/homeassistant/components/meteo_france/translations/pt.json index 025d58f5197..3137ef26505 100644 --- a/homeassistant/components/meteo_france/translations/pt.json +++ b/homeassistant/components/meteo_france/translations/pt.json @@ -4,8 +4,7 @@ "user": { "data": { "city": "Cidade" - }, - "title": "" + } } } } diff --git a/homeassistant/components/meteo_france/translations/tr.json b/homeassistant/components/meteo_france/translations/tr.json new file mode 100644 index 00000000000..57fc9f76881 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "empty": "\u015eehir aramas\u0131nda sonu\u00e7 yok: l\u00fctfen \u015fehir alan\u0131n\u0131 kontrol edin" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/ru.json b/homeassistant/components/metoffice/translations/ru.json index 2b9716439eb..68c5ef6268d 100644 --- a/homeassistant/components/metoffice/translations/ru.json +++ b/homeassistant/components/metoffice/translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/mikrotik/translations/no.json b/homeassistant/components/mikrotik/translations/no.json index 1e528fa4986..0ae25436433 100644 --- a/homeassistant/components/mikrotik/translations/no.json +++ b/homeassistant/components/mikrotik/translations/no.json @@ -14,7 +14,6 @@ "host": "Vert", "name": "Navn", "password": "Passord", - "port": "", "username": "Brukernavn", "verify_ssl": "Bruk ssl" }, diff --git a/homeassistant/components/mikrotik/translations/pt-BR.json b/homeassistant/components/mikrotik/translations/pt-BR.json index 06ad1cba6d0..48663023cf3 100644 --- a/homeassistant/components/mikrotik/translations/pt-BR.json +++ b/homeassistant/components/mikrotik/translations/pt-BR.json @@ -12,6 +12,7 @@ "user": { "data": { "name": "Nome", + "username": "Usu\u00e1rio", "verify_ssl": "Usar SSL" }, "title": "Configurar roteador Mikrotik" diff --git a/homeassistant/components/mill/translations/ru.json b/homeassistant/components/mill/translations/ru.json index d84166e1cdf..cdc89033330 100644 --- a/homeassistant/components/mill/translations/ru.json +++ b/homeassistant/components/mill/translations/ru.json @@ -4,7 +4,7 @@ "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": { - "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/monoprice/translations/no.json b/homeassistant/components/monoprice/translations/no.json index acd4bde8774..45954d9840b 100644 --- a/homeassistant/components/monoprice/translations/no.json +++ b/homeassistant/components/monoprice/translations/no.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "port": "", "source_1": "Navn p\u00e5 kilden #1", "source_2": "Navn p\u00e5 kilden #2", "source_3": "Navn p\u00e5 kilden #3", diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index ef5d173dce7..3488c323911 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -66,11 +66,13 @@ }, "options": { "data": { + "birth_enable": "Abilita messaggio di nascita", "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_enable": "Abilita messaggio di nascita", "will_payload": "Payload del messaggio will", "will_qos": "QoS del messaggio will", "will_retain": "Persistenza del messaggio will", diff --git a/homeassistant/components/mqtt/translations/lb.json b/homeassistant/components/mqtt/translations/lb.json index 1ee1b8b1c1e..ce1bbabfdc9 100644 --- a/homeassistant/components/mqtt/translations/lb.json +++ b/homeassistant/components/mqtt/translations/lb.json @@ -66,11 +66,13 @@ }, "options": { "data": { + "birth_enable": "Gebuert Message aktiv\u00e9ieren", "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_enable": "Gebuert Message aktiv\u00e9ieren", "will_payload": "Will message payload", "will_qos": "Will message QoS", "will_retain": "Will message retain", diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index b1863b90d1c..c5ee982b63d 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -12,7 +12,6 @@ "broker": "Megler", "discovery": "Aktiver oppdagelse", "password": "Passord", - "port": "", "username": "Brukernavn" }, "description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler." @@ -59,7 +58,6 @@ "data": { "broker": "Megler", "password": "Passord", - "port": "", "username": "Brukernavn" }, "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler." diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index 3a441d0c1f1..de739963cae 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -13,7 +13,7 @@ "discovery": "Ativar descoberta", "password": "Senha", "port": "Porta", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" }, "description": "Por favor, insira as informa\u00e7\u00f5es de conex\u00e3o do seu agente MQTT." }, diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 07458569f73..02978a22327 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -66,11 +66,13 @@ }, "options": { "data": { + "birth_enable": "\u958b\u555f Birth \u8a0a\u606f", "birth_payload": "Birth \u8a0a\u606f payload", "birth_qos": "Birth \u8a0a\u606f QoS", "birth_retain": "Birth \u8a0a\u606f Retain", "birth_topic": "Birth \u8a0a\u606f\u4e3b\u984c", "discovery": "\u958b\u555f\u63a2\u7d22", + "will_enable": "\u958b\u555f Birth \u8a0a\u606f", "will_payload": "Will \u8a0a\u606f payload", "will_qos": "Will \u8a0a\u606f QoS", "will_retain": "Will \u8a0a\u606f Retain", diff --git a/homeassistant/components/myq/translations/pt-BR.json b/homeassistant/components/myq/translations/pt-BR.json new file mode 100644 index 00000000000..932b4b8a72e --- /dev/null +++ b/homeassistant/components/myq/translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pt-BR.json b/homeassistant/components/neato/translations/pt-BR.json new file mode 100644 index 00000000000..932b4b8a72e --- /dev/null +++ b/homeassistant/components/neato/translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index e01875998cc..a9e54b31bd7 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "create_entry": { "default": "Autenticazione riuscita" diff --git a/homeassistant/components/netatmo/translations/lb.json b/homeassistant/components/netatmo/translations/lb.json index 73032f898d3..f46b8627e0c 100644 --- a/homeassistant/components/netatmo/translations/lb.json +++ b/homeassistant/components/netatmo/translations/lb.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL", - "missing_configuration": "D\u00ebs Komponent ass net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." + "missing_configuration": "D\u00ebs Komponent ass net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." }, "create_entry": { "default": "Erfollegr\u00e4ich authentifiz\u00e9iert." diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index c54488cdcbc..fd528ed4cfc 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/nexia/translations/pt-BR.json b/homeassistant/components/nexia/translations/pt-BR.json new file mode 100644 index 00000000000..932b4b8a72e --- /dev/null +++ b/homeassistant/components/nexia/translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ca.json b/homeassistant/components/nightscout/translations/ca.json new file mode 100644 index 00000000000..eb06d94de3b --- /dev/null +++ b/homeassistant/components/nightscout/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json index 439d42110bc..b7947c84997 100644 --- a/homeassistant/components/nightscout/translations/en.json +++ b/homeassistant/components/nightscout/translations/en.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" }, "flow_title": "Nightscout", "step": { diff --git a/homeassistant/components/nightscout/translations/es.json b/homeassistant/components/nightscout/translations/es.json new file mode 100644 index 00000000000..9a03055bac4 --- /dev/null +++ b/homeassistant/components/nightscout/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/it.json b/homeassistant/components/nightscout/translations/it.json new file mode 100644 index 00000000000..e0305d1b5e2 --- /dev/null +++ b/homeassistant/components/nightscout/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json new file mode 100644 index 00000000000..17dee71d640 --- /dev/null +++ b/homeassistant/components/nightscout/translations/ko.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/lb.json b/homeassistant/components/nightscout/translations/lb.json new file mode 100644 index 00000000000..76f6a7f233c --- /dev/null +++ b/homeassistant/components/nightscout/translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/no.json b/homeassistant/components/nightscout/translations/no.json new file mode 100644 index 00000000000..a586083f569 --- /dev/null +++ b/homeassistant/components/nightscout/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes.", + "unknown": "Uventet feil" + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/pt-BR.json b/homeassistant/components/nightscout/translations/pt-BR.json new file mode 100644 index 00000000000..68dc0756725 --- /dev/null +++ b/homeassistant/components/nightscout/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/ru.json b/homeassistant/components/nightscout/translations/ru.json new file mode 100644 index 00000000000..cf904f0134c --- /dev/null +++ b/homeassistant/components/nightscout/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/tr.json b/homeassistant/components/nightscout/translations/tr.json new file mode 100644 index 00000000000..585aace899d --- /dev/null +++ b/homeassistant/components/nightscout/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flan\u0131lamad\u0131" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json new file mode 100644 index 00000000000..fa9f3d12427 --- /dev/null +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "url": "\u7db2\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notify/translations/pt-BR.json b/homeassistant/components/notify/translations/pt-BR.json index d92f73d4a77..7dfa16bc564 100644 --- a/homeassistant/components/notify/translations/pt-BR.json +++ b/homeassistant/components/notify/translations/pt-BR.json @@ -1,3 +1,3 @@ { - "title": "Notificar" + "title": "Notifica\u00e7\u00f5es" } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/pt-BR.json b/homeassistant/components/notion/translations/pt-BR.json index ee87b78aa90..db9ba8c1a47 100644 --- a/homeassistant/components/notion/translations/pt-BR.json +++ b/homeassistant/components/notion/translations/pt-BR.json @@ -8,7 +8,7 @@ "user": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio/ende\u00e7o de e-mail" + "username": "Usu\u00e1rio/ende\u00e7o de e-mail" }, "title": "Preencha suas informa\u00e7\u00f5es" } diff --git a/homeassistant/components/nuheat/translations/pt-BR.json b/homeassistant/components/nuheat/translations/pt-BR.json index 0fb1e678edd..7963212e49c 100644 --- a/homeassistant/components/nuheat/translations/pt-BR.json +++ b/homeassistant/components/nuheat/translations/pt-BR.json @@ -12,7 +12,8 @@ "step": { "user": { "data": { - "serial_number": "N\u00famero de s\u00e9rie do termostato." + "serial_number": "N\u00famero de s\u00e9rie do termostato.", + "username": "Usu\u00e1rio" }, "description": "Voc\u00ea precisar\u00e1 obter o n\u00famero de s\u00e9rie ou ID num\u00e9rico do seu termostato acessando https://MyNuHeat.com e selecionando seu(s) termostato(s).", "title": "Conecte-se ao NuHeat" diff --git a/homeassistant/components/nut/translations/no.json b/homeassistant/components/nut/translations/no.json index 6fd749442c3..9bf7f981dca 100644 --- a/homeassistant/components/nut/translations/no.json +++ b/homeassistant/components/nut/translations/no.json @@ -16,7 +16,6 @@ }, "ups": { "data": { - "alias": "", "resources": "Ressurser" }, "title": "Velg UPS som skal overv\u00e5kes" @@ -25,7 +24,6 @@ "data": { "host": "Vert", "password": "Passord", - "port": "", "username": "Brukernavn" }, "title": "Koble til NUT-serveren" diff --git a/homeassistant/components/onvif/translations/no.json b/homeassistant/components/onvif/translations/no.json index 4f605a518d7..dcb3a102829 100644 --- a/homeassistant/components/onvif/translations/no.json +++ b/homeassistant/components/onvif/translations/no.json @@ -34,8 +34,7 @@ "manual_input": { "data": { "host": "Vert", - "name": "Navn", - "port": "" + "name": "Navn" }, "title": "Konfigurere ONVIF-enhet" }, diff --git a/homeassistant/components/onvif/translations/pt-BR.json b/homeassistant/components/onvif/translations/pt-BR.json index 7d8689cfeae..5f2cce217ea 100644 --- a/homeassistant/components/onvif/translations/pt-BR.json +++ b/homeassistant/components/onvif/translations/pt-BR.json @@ -14,7 +14,7 @@ "auth": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" }, "title": "Configurar autentica\u00e7\u00e3o" }, diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index fc82ed5bb8a..7a22e22a4aa 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "user": { + "description": "G\u00f6nder d\u00fc\u011fmesine t\u0131klad\u0131\u011f\u0131n\u0131zda, Profil S'yi destekleyen ONVIF cihazlar\u0131 i\u00e7in a\u011f\u0131n\u0131zda arama yapaca\u011f\u0131z. \n\n Baz\u0131 \u00fcreticiler varsay\u0131lan olarak ONVIF'i devre d\u0131\u015f\u0131 b\u0131rakmaya ba\u015flad\u0131. L\u00fctfen kameran\u0131z\u0131n yap\u0131land\u0131rmas\u0131nda ONVIF'in etkinle\u015ftirildi\u011finden emin olun." + } + } + }, "options": { "step": { "onvif_devices": { diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index f0ecf0277b2..ed4dbd4abfb 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Bane eller URL-adresse", - "id": "", "name": "Navn" - }, - "title": "" + } } } }, diff --git a/homeassistant/components/opentherm_gw/translations/pt.json b/homeassistant/components/opentherm_gw/translations/pt.json index 960e3a9cf5c..0342dd3ebcb 100644 --- a/homeassistant/components/opentherm_gw/translations/pt.json +++ b/homeassistant/components/opentherm_gw/translations/pt.json @@ -3,7 +3,6 @@ "step": { "init": { "data": { - "id": "", "name": "Nome" } } diff --git a/homeassistant/components/ovo_energy/translations/ca.json b/homeassistant/components/ovo_energy/translations/ca.json index 11ff423f1dd..feb03a790f2 100644 --- a/homeassistant/components/ovo_energy/translations/ca.json +++ b/homeassistant/components/ovo_energy/translations/ca.json @@ -1,8 +1,9 @@ { "config": { "error": { - "authorization_error": "Error d'autoritzaci\u00f3. Comproveu les vostres credencials.", - "connection_error": "No s'ha pogut connectar a OVO Energy." + "already_configured": "El compte ja ha estat configurat", + "authorization_error": "Error d'autoritzaci\u00f3. Comprova les teves credencials.", + "connection_error": "Ha fallat la connexi\u00f3" }, "step": { "user": { @@ -11,7 +12,7 @@ "username": "Nom d'usuari" }, "description": "Configura una inst\u00e0ncia OVO Energy per accedir al consum energ\u00e8tic.", - "title": "Afegir OVO Energy" + "title": "Afegeix compte d'OVO Energy" } } } diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json index 0132f3582b6..fd1a0225867 100644 --- a/homeassistant/components/ovo_energy/translations/en.json +++ b/homeassistant/components/ovo_energy/translations/en.json @@ -1,19 +1,19 @@ { "config": { "error": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_configured": "Account is already configured", "authorization_error": "Authorization error. Check your credentials.", - "connection_error": "[%key:common::config_flow::error::cannot_connect%]" + "connection_error": "Failed to connect" }, "step": { "user": { "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "password": "Password", + "username": "Username" }, "description": "Set up an OVO Energy instance to access your energy usage.", "title": "Add OVO Energy Account" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json new file mode 100644 index 00000000000..2eb715d7113 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "authorization_error": "Error de autorizaci\u00f3n. Comprueba tus credenciales.", + "connection_error": "No se ha podido conectar a OVO Energy." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Configurar una instancia de OVO Energy para acceder a su consumo de energ\u00eda.", + "title": "A\u00f1adir OVO Energy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/it.json b/homeassistant/components/ovo_energy/translations/it.json new file mode 100644 index 00000000000..1e2ce3cd442 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "authorization_error": "Errore di autorizzazione. Controlla le tue credenziali.", + "connection_error": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Configura un'istanza OVO Energy per accedere al tuo consumo energetico.", + "title": "Aggiungi Conto Energetico OVO" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/ko.json b/homeassistant/components/ovo_energy/translations/ko.json new file mode 100644 index 00000000000..09d002bc161 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "title": "OVO Energy \uacc4\uc815 \ucd94\uac00\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/lb.json b/homeassistant/components/ovo_energy/translations/lb.json new file mode 100644 index 00000000000..74d4f949b43 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Kont ass scho konfigur\u00e9iert", + "authorization_error": "Feeler bei der Autorisatioun. Iwwerpr\u00e9if deng Verbindungs Informatiounen.", + "connection_error": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "description": "Eng OVO Energie Instanz arrichten fir d\u00e4in Energie Verbrauch ze gesinn.", + "title": "OVO Energy Kont dob\u00e4isetzen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json index 50e1d74b0e9..fc61e40506c 100644 --- a/homeassistant/components/ovo_energy/translations/no.json +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -1,8 +1,9 @@ { "config": { "error": { + "already_configured": "Kontoen er allerede konfigurert", "authorization_error": "Autoriseringsfeil. Sjekk legitimasjonsbeskrivelsen.", - "connection_error": "Kunne ikke koble til OVO Energy." + "connection_error": "Tilkobling mislyktes." }, "step": { "user": { @@ -11,7 +12,7 @@ "username": "Brukernavn" }, "description": "Sett opp en OVO Energy-forekomst for \u00e5 f\u00e5 tilgang til energibruken din.", - "title": "Legg til OVO Energy" + "title": "Legg til OVO Energikonto" } } } diff --git a/homeassistant/components/ovo_energy/translations/pt-BR.json b/homeassistant/components/ovo_energy/translations/pt-BR.json new file mode 100644 index 00000000000..0a40f65af5e --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "connection_error": "Falha na conex\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json index cf69cb2973d..83d0e16f4c4 100644 --- a/homeassistant/components/ovo_energy/translations/ru.json +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -1,8 +1,9 @@ { "config": { "error": { + "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.", "authorization_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a OVO Energy." + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/ovo_energy/translations/zh-Hant.json b/homeassistant/components/ovo_energy/translations/zh-Hant.json new file mode 100644 index 00000000000..4aa84207cf8 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "authorization_error": "\u8a8d\u8b49\u932f\u8aa4\u3002\u8acb\u78ba\u8a8d\u6191\u8b49\u3002", + "connection_error": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a OVO Energy \u8a2d\u5099\u4ee5\u76e3\u63a7\u80fd\u6e90\u4f7f\u7528\u72c0\u6cc1\u3002", + "title": "\u65b0\u589e OVO Energy \u5e33\u865f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/ca.json b/homeassistant/components/panasonic_viera/translations/ca.json index aba8f5c38f0..8b703ef6652 100644 --- a/homeassistant/components/panasonic_viera/translations/ca.json +++ b/homeassistant/components/panasonic_viera/translations/ca.json @@ -15,7 +15,7 @@ "pin": "PIN" }, "description": "Introdueix el PIN que apareix a la pantalla del televisor", - "title": "Emparellament" + "title": "Vinculaci\u00f3" }, "user": { "data": { diff --git a/homeassistant/components/panasonic_viera/translations/no.json b/homeassistant/components/panasonic_viera/translations/no.json index 039adbd2ad3..91a01793c1c 100644 --- a/homeassistant/components/panasonic_viera/translations/no.json +++ b/homeassistant/components/panasonic_viera/translations/no.json @@ -11,9 +11,6 @@ }, "step": { "pairing": { - "data": { - "pin": "" - }, "description": "Angi PIN-koden som vises p\u00e5 TV-en", "title": "Sammenkobling" }, @@ -26,6 +23,5 @@ "title": "Sett opp TV-en din" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/person/translations/nb.json b/homeassistant/components/person/translations/nb.json index 6d380619114..98c0b9241fb 100644 --- a/homeassistant/components/person/translations/nb.json +++ b/homeassistant/components/person/translations/nb.json @@ -4,6 +4,5 @@ "home": "Hjemme", "not_home": "Borte" } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/person/translations/no.json b/homeassistant/components/person/translations/no.json index 6d380619114..98c0b9241fb 100644 --- a/homeassistant/components/person/translations/no.json +++ b/homeassistant/components/person/translations/no.json @@ -4,6 +4,5 @@ "home": "Hjemme", "not_home": "Borte" } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index 387b6c0d1eb..4655d254070 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -13,7 +13,6 @@ "host": "Vert", "location": "Beliggenhet", "name": "Navn", - "port": "", "ssl": "Bruk SSL", "verify_ssl": "Verifisere SSL-sertifikat" } diff --git a/homeassistant/components/pi_hole/translations/pt-BR.json b/homeassistant/components/pi_hole/translations/pt-BR.json index a09bc3c2dec..8b7cd1004ea 100644 --- a/homeassistant/components/pi_hole/translations/pt-BR.json +++ b/homeassistant/components/pi_hole/translations/pt-BR.json @@ -9,8 +9,9 @@ "step": { "user": { "data": { - "api_key": "Chave de API (Opcional)", + "api_key": "Chave de API", "host": "Endere\u00e7o (IP)", + "location": "Localiza\u00e7\u00e3o", "name": "Nome", "port": "Porta", "ssl": "Usar SSL", diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json index ee3fa80251d..6f7dd24e5e2 100644 --- a/homeassistant/components/pi_hole/translations/ru.json +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/plant/translations/nb.json b/homeassistant/components/plant/translations/nb.json index c8f9e3e1d44..0d144184263 100644 --- a/homeassistant/components/plant/translations/nb.json +++ b/homeassistant/components/plant/translations/nb.json @@ -1,7 +1,6 @@ { "state": { "_": { - "ok": "", "problem": "Problem" } }, diff --git a/homeassistant/components/plant/translations/no.json b/homeassistant/components/plant/translations/no.json index e82299e36e9..0a08a5eaed4 100644 --- a/homeassistant/components/plant/translations/no.json +++ b/homeassistant/components/plant/translations/no.json @@ -1,9 +1,3 @@ { - "state": { - "_": { - "ok": "", - "problem": "" - } - }, "title": "Plantemonitor" } \ No newline at end of file diff --git a/homeassistant/components/plex/translations/nl.json b/homeassistant/components/plex/translations/nl.json index 1d5a9351426..bf3018c6672 100644 --- a/homeassistant/components/plex/translations/nl.json +++ b/homeassistant/components/plex/translations/nl.json @@ -12,7 +12,7 @@ "no_servers": "Geen servers gekoppeld aan account", "not_found": "Plex-server niet gevonden" }, - "flow_title": "{naam} ({host})", + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json index c7374e27b60..027260ea34d 100644 --- a/homeassistant/components/plex/translations/no.json +++ b/homeassistant/components/plex/translations/no.json @@ -19,7 +19,6 @@ "manual_setup": { "data": { "host": "Vert", - "port": "", "ssl": "Bruk SSL", "token": "Token (valgfritt)", "verify_ssl": "Verifisere SSL-sertifikat" @@ -27,21 +26,16 @@ "title": "Manuell Plex-konfigurasjon" }, "select_server": { - "data": { - "server": "" - }, "description": "Flere servere tilgjengelig, velg en:", "title": "Velg Plex-server" }, "user": { - "description": "Fortsett til [plex.tv] (https://plex.tv) for \u00e5 koble en Plex-server.", - "title": "" + "description": "Fortsett til [plex.tv] (https://plex.tv) for \u00e5 koble en Plex-server." }, "user_advanced": { "data": { "setup_method": "Oppsettmetode" - }, - "title": "" + } } } }, diff --git a/homeassistant/components/plex/translations/pt-BR.json b/homeassistant/components/plex/translations/pt-BR.json index cc965a12740..eac953579c0 100644 --- a/homeassistant/components/plex/translations/pt-BR.json +++ b/homeassistant/components/plex/translations/pt-BR.json @@ -7,6 +7,7 @@ "step": { "manual_setup": { "data": { + "port": "Porta", "ssl": "Usar SSL", "token": "Token (Opcional)", "verify_ssl": "Verifique o certificado SSL" diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index 0a5f0e58c76..40a7a0da317 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -16,7 +16,7 @@ "password": "ID de Smile" }, "description": "Detalls", - "title": "Connecta\u2019t amb el Smile" + "title": "Connecta't amb el Smile" } } } diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index 7fd5ac6ae28..694e6348cae 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -8,12 +8,10 @@ "invalid_auth": "Ugyldig godkjenning, sjekk din 8-tegns Smile ID", "unknown": "Uventet feil" }, - "flow_title": "", "step": { "user": { "data": { - "host": "Smile IP-adresse", - "password": "" + "host": "Smile IP-adresse" }, "description": "Detaljer", "title": "Koble til Smile" diff --git a/homeassistant/components/plum_lightpad/translations/ru.json b/homeassistant/components/plum_lightpad/translations/ru.json index 12f2e4a01c5..3e14039ae00 100644 --- a/homeassistant/components/plum_lightpad/translations/ru.json +++ b/homeassistant/components/plum_lightpad/translations/ru.json @@ -4,7 +4,7 @@ "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." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/poolsense/translations/no.json b/homeassistant/components/poolsense/translations/no.json index 38adc04c1db..a199f7384a8 100644 --- a/homeassistant/components/poolsense/translations/no.json +++ b/homeassistant/components/poolsense/translations/no.json @@ -12,8 +12,7 @@ "email": "E-post", "password": "Passord" }, - "description": "[%key:common::config_flow::description%]", - "title": "" + "description": "[%key:common::config_flow::description%]" } } } diff --git a/homeassistant/components/poolsense/translations/ru.json b/homeassistant/components/poolsense/translations/ru.json index 9c474d7d8e7..ed7adab0ccf 100644 --- a/homeassistant/components/poolsense/translations/ru.json +++ b/homeassistant/components/poolsense/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." diff --git a/homeassistant/components/ps4/translations/no.json b/homeassistant/components/ps4/translations/no.json index 814f09095a2..4bf3b02b0b5 100644 --- a/homeassistant/components/ps4/translations/no.json +++ b/homeassistant/components/ps4/translations/no.json @@ -15,26 +15,21 @@ }, "step": { "creds": { - "description": "Legitimasjon n\u00f8dvendig. Trykk 'Send' og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg 'Home-Assistant' enheten for \u00e5 fortsette.", - "title": "" + "description": "Legitimasjon n\u00f8dvendig. Trykk 'Send' og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg 'Home-Assistant' enheten for \u00e5 fortsette." }, "link": { "data": { - "code": "", "ip_address": "IP adresse", - "name": "Navn", - "region": "" + "name": "Navn" }, - "description": "Fyll inn PlayStation 4-informasjonen. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsoll. Naviger deretter til 'Mobile App Connection Settings' og velg 'Add Device'. Fyll inn PIN-koden som vises. Se [dokumentasjonen](https://www.home-assistant.io/components/ps4/) for mer informasjon.", - "title": "" + "description": "Fyll inn PlayStation 4-informasjonen. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsoll. Naviger deretter til 'Mobile App Connection Settings' og velg 'Add Device'. Fyll inn PIN-koden som vises. Se [dokumentasjonen](https://www.home-assistant.io/components/ps4/) for mer informasjon." }, "mode": { "data": { "ip_address": "IP-adresse (La st\u00e5 tom hvis du bruker Automatisk Oppdagelse).", "mode": "Konfigureringsmodus" }, - "description": "Velg modus for konfigurasjon. Feltet IP-adresse kan st\u00e5 tomt dersom du velger Automatisk Oppdagelse, da enheter vil bli oppdaget automatisk.", - "title": "" + "description": "Velg modus for konfigurasjon. Feltet IP-adresse kan st\u00e5 tomt dersom du velger Automatisk Oppdagelse, da enheter vil bli oppdaget automatisk." } } } diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index bc80cdedb31..294c2726396 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -11,8 +11,7 @@ "user": { "data": { "ip_address": "Vertsnavn eller IP-adresse", - "password": "Passord", - "port": "" + "password": "Passord" }, "title": "Fyll ut informasjonen din" } diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json new file mode 100644 index 00000000000..14e637f5f98 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/ca.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json index 263b2a9467b..1344d2f6988 100644 --- a/homeassistant/components/rfxtrx/translations/en.json +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - }, - "error": {}, - "step": {} + "already_configured": "Device is already configured" + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json new file mode 100644 index 00000000000..e8e23bf8343 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json new file mode 100644 index 00000000000..cbea61b08cb --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "one": "uno", + "other": "altri" + }, + "step": { + "one": "uno", + "other": "altri" + } + }, + "one": "uno", + "other": "altri" +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/lb.json b/homeassistant/components/rfxtrx/translations/lb.json new file mode 100644 index 00000000000..6469543442e --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/lb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json new file mode 100644 index 00000000000..6ba5a1f3978 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/no.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/ru.json b/homeassistant/components/rfxtrx/translations/ru.json new file mode 100644 index 00000000000..4ad85f691be --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/ru.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json new file mode 100644 index 00000000000..1ab3e1f720f --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/ca.json b/homeassistant/components/ring/translations/ca.json index 454956fe46c..bcd3bd43af4 100644 --- a/homeassistant/components/ring/translations/ca.json +++ b/homeassistant/components/ring/translations/ca.json @@ -19,7 +19,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "title": "Inici de sessi\u00f3 amb Ring" + "title": "Inici de sessi\u00f3 amb un compte de Ring" } } } diff --git a/homeassistant/components/ring/translations/pt-BR.json b/homeassistant/components/ring/translations/pt-BR.json new file mode 100644 index 00000000000..abb894549cc --- /dev/null +++ b/homeassistant/components/ring/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/ca.json b/homeassistant/components/risco/translations/ca.json new file mode 100644 index 00000000000..693885d51cb --- /dev/null +++ b/homeassistant/components/risco/translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "pin": "Codi PIN", + "username": "Nom d'usuari" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code_arm_required": "Demana codi PIN per activar", + "code_disarm_required": "Demana codi PIN per desactivar", + "scan_interval": "Freq\u00fc\u00e8ncia de sondeig a Risco (en segons)" + }, + "title": "Opcions de configuraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/en.json b/homeassistant/components/risco/translations/en.json new file mode 100644 index 00000000000..82422dcbe68 --- /dev/null +++ b/homeassistant/components/risco/translations/en.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "pin": "Pin code", + "username": "Username" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code_arm_required": "Require pin code to arm", + "code_disarm_required": "Require pin code to disarm", + "scan_interval": "How often to poll Risco (in seconds)" + }, + "title": "Configure options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json new file mode 100644 index 00000000000..c308cddeb76 --- /dev/null +++ b/homeassistant/components/risco/translations/es.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "pin": "C\u00f3digo pin", + "username": "Nombre de usuario" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code_arm_required": "Requiere un c\u00f3digo PIN para armar", + "code_disarm_required": "Requiere un c\u00f3digo PIN para desarmar", + "scan_interval": "Con qu\u00e9 frecuencia sondear Risco (en segundos)" + }, + "title": "Configurar opciones" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/fr.json b/homeassistant/components/risco/translations/fr.json new file mode 100644 index 00000000000..827893b2194 --- /dev/null +++ b/homeassistant/components/risco/translations/fr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "P\u00e9riph\u00e9rique d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Echec de connexion", + "invalid_auth": "Authentification erron\u00e9e", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "pin": "Code Pin", + "username": "Nom d'utilisateur" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u00c0 quelle fr\u00e9quence interroger Risco (en secondes)" + }, + "title": "Configurer les options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/it.json b/homeassistant/components/risco/translations/it.json new file mode 100644 index 00000000000..9edc795f4e9 --- /dev/null +++ b/homeassistant/components/risco/translations/it.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "pin": "Codice PIN", + "username": "Nome utente" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code_arm_required": "Richiedi codice PIN per armare", + "code_disarm_required": "Richiedi codice PIN per disarmare", + "scan_interval": "Con che frequenza interrogare Risco (in secondi)" + }, + "title": "Configura le opzioni" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/lb.json b/homeassistant/components/risco/translations/lb.json new file mode 100644 index 00000000000..933af1f4a38 --- /dev/null +++ b/homeassistant/components/risco/translations/lb.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "pin": "Pin Code", + "username": "Benotzernumm" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code_arm_required": "Pin Code n\u00e9ideg fir unzeschalten", + "code_disarm_required": "Pin Code n\u00e9ideg fir auszeschalten" + }, + "title": "Optioune konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/no.json b/homeassistant/components/risco/translations/no.json new file mode 100644 index 00000000000..5ab1d507b29 --- /dev/null +++ b/homeassistant/components/risco/translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes.", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "pin": "PIN kode", + "username": "Brukernavn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code_arm_required": "Krev PIN-kode for \u00e5 koble til", + "code_disarm_required": "Krev PIN-kode for \u00e5 deaktivere", + "scan_interval": "Hvor ofte skal man unders\u00f8ke Risco (i l\u00f8pet av sekunder)" + }, + "title": "Konfigurer alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/ru.json b/homeassistant/components/risco/translations/ru.json new file mode 100644 index 00000000000..b4f55347512 --- /dev/null +++ b/homeassistant/components/risco/translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "pin": "PIN-\u043a\u043e\u0434", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code_arm_required": "\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c PIN-\u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "code_disarm_required": "\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c PIN-\u043a\u043e\u0434 \u0434\u043b\u044f \u0441\u043d\u044f\u0442\u0438\u044f \u0441 \u043e\u0445\u0440\u0430\u043d\u044b", + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/zh-Hant.json b/homeassistant/components/risco/translations/zh-Hant.json new file mode 100644 index 00000000000..0a703baf68e --- /dev/null +++ b/homeassistant/components/risco/translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "pin": "PIN \u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "code_arm_required": "\u9700\u8981\u8f38\u5165 PIN \u4ee5\u8b66\u6212", + "code_disarm_required": "\u9700\u8981\u8f38\u5165 PIN \u4ee5\u89e3\u9664\u8b66\u6212", + "scan_interval": "\u66f4\u65b0 Risco \u983b\u7387\uff08\u79d2\uff09" + }, + "title": "\u8a2d\u5b9a\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index 43e0ea1f1c8..10fb51557da 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -10,8 +10,7 @@ "flow_title": "Roku: {name}", "step": { "ssdp_confirm": { - "description": "Vil du sette opp {name} ?", - "title": "" + "description": "Vil du sette opp {name} ?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json index bddfab208c3..b5dcddbe555 100644 --- a/homeassistant/components/roku/translations/ru.json +++ b/homeassistant/components/roku/translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roon/translations/ca.json b/homeassistant/components/roon/translations/ca.json new file mode 100644 index 00000000000..b783c9a6229 --- /dev/null +++ b/homeassistant/components/roon/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "duplicate_entry": "Aquest amfitri\u00f3 ja ha estat afegit.", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "link": { + "description": "Has d'autoritzar Home Assistant a Roon. Despr\u00e9s de fer clic a envia, ves a l'aplicaci\u00f3 Roon Core, obre la Configuraci\u00f3 i activa Home Assistant a la pestanya d'Extensions.", + "title": "Autoritza Home Assistant a Roon" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Introdueix el nom d'amfitri\u00f3 o la IP del servidor Roon", + "title": "Configura un servidor Roon" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/en.json b/homeassistant/components/roon/translations/en.json index 230fe87809a..b7ae64f0de1 100644 --- a/homeassistant/components/roon/translations/en.json +++ b/homeassistant/components/roon/translations/en.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "Device is already configured" }, "error": { "duplicate_entry": "That host has already been added.", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" }, "step": { "link": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "[%key:common::config_flow::data::host%]" + "host": "Host" }, "description": "Please enter your Roon server Hostname or IP.", "title": "Configure Roon Server" diff --git a/homeassistant/components/roon/translations/es.json b/homeassistant/components/roon/translations/es.json new file mode 100644 index 00000000000..9c8f4346118 --- /dev/null +++ b/homeassistant/components/roon/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "duplicate_entry": "Ese host ya ha sido a\u00f1adido.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "link": { + "description": "Debes autorizar Home Assistant en Roon. Despu\u00e9s de pulsar en Enviar, ve a la aplicaci\u00f3n Roon Core, abre Configuraci\u00f3n y activa HomeAssistant en la pesta\u00f1a Extensiones.", + "title": "Autorizar HomeAssistant en Roon" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Introduce el nombre de Host o IP del servidor Roon.", + "title": "Configurar el Servidor Roon" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/fr.json b/homeassistant/components/roon/translations/fr.json new file mode 100644 index 00000000000..2db0855979e --- /dev/null +++ b/homeassistant/components/roon/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "duplicate_entry": "Cet h\u00f4te a d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9." + }, + "step": { + "link": { + "description": "Vous devez autoriser Home Assistant dans Roon. Apr\u00e8s avoir cliqu\u00e9 sur soumettre, acc\u00e9dez \u00e0 l'application Roon Core, ouvrez Param\u00e8tres et activez Home Assistant dans l'onglet Extensions.", + "title": "Autoriser Home Assistant dans Roon" + }, + "user": { + "description": "Veuillez entrer votre nom d\u2019h\u00f4te ou votre adresse IP sur votre serveur Roon.", + "title": "Configurer le serveur Roon" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/it.json b/homeassistant/components/roon/translations/it.json new file mode 100644 index 00000000000..cfa1f7a3844 --- /dev/null +++ b/homeassistant/components/roon/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "duplicate_entry": "Questo host \u00e8 gi\u00e0 stato aggiunto.", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "link": { + "description": "\u00c8 necessario autorizzare l'Assistente Home in Roon. Dopo aver fatto clic su Invia, passare all'applicazione Roon Core, aprire Impostazioni e abilitare HomeAssistant nella scheda Estensioni.", + "title": "Autorizzare HomeAssistant in Roon" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Inserisci il nome host o l'IP del tuo server Roon.", + "title": "Configurare Il server Roon" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/ko.json b/homeassistant/components/roon/translations/ko.json new file mode 100644 index 00000000000..50c22e9e256 --- /dev/null +++ b/homeassistant/components/roon/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/lb.json b/homeassistant/components/roon/translations/lb.json new file mode 100644 index 00000000000..e8c3bc1825b --- /dev/null +++ b/homeassistant/components/roon/translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "duplicate_entry": "D\u00ebsen Apparat gouf scho dob\u00e4igesat.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "link": { + "description": "Du muss Home Assistant am Roon autoris\u00e9ieren. Nodeems Du op ofsch\u00e9cke geklickt hues, g\u00e9i an d'Roon Applikatioun, an d'Astellungen an aktiv\u00e9ier HomeAssistant an den Extensiounen.", + "title": "HomeAssistant am Roon erlaaben" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "G\u00ebff den Numm oder IP-Adress vun dengem Roon Server un.", + "title": "Roon Server ariichten" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/nl.json b/homeassistant/components/roon/translations/nl.json new file mode 100644 index 00000000000..73abdb3d49b --- /dev/null +++ b/homeassistant/components/roon/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al toegevoegd" + }, + "error": { + "duplicate_entry": "Die host is al toegevoegd.", + "invalid_auth": "Ongeldige authencatie", + "unknown": "Onverwachte fout" + }, + "step": { + "link": { + "description": "U moet Home Assistant autoriseren in Roon. Nadat je op verzenden hebt geklikt, ga je naar de Roon Core-applicatie, open je Instellingen en schakel je Home Assistant in op het tabblad Extensies.", + "title": "Autoriseer Home Assistant in Roon" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Voer de hostnaam of het IP-adres van uw Roon-server in.", + "title": "Configureer Roon Server" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json new file mode 100644 index 00000000000..cdf62b8c3c2 --- /dev/null +++ b/homeassistant/components/roon/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "duplicate_entry": "Denne verten er allerede lagt til.", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "link": { + "description": "Du m\u00e5 autorisere home assistant i Roon. N\u00e5r du klikker send inn, g\u00e5r du til Roon Core-programmet, \u00e5pner Innstillinger og aktiverer HomeAssistant p\u00e5 Utvidelser-fanen.", + "title": "Autoriser HomeAssistant i Roon" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Vennligst skriv inn Roon-serverens vertsnavn eller IP.", + "title": "Konfigurer Roon Server" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/pt-BR.json b/homeassistant/components/roon/translations/pt-BR.json new file mode 100644 index 00000000000..5a11826f285 --- /dev/null +++ b/homeassistant/components/roon/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 foi configurado" + }, + "error": { + "duplicate_entry": "Esse host j\u00e1 foi adicionado.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Ocorreu um erro inexperado" + }, + "step": { + "link": { + "description": "Voc\u00ea deve autorizar o Home Assistant no Roon. Depois de clicar em enviar, v\u00e1 para o aplicativo Roon principal, abra Configura\u00e7\u00f5es e habilite o HomeAssistant na aba Extens\u00f5es.", + "title": "Autorizar HomeAssistant no Roon" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Por favor, digite seu hostname ou IP do servidor Roon.", + "title": "Configurar Servidor Roon" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/ru.json b/homeassistant/components/roon/translations/ru.json new file mode 100644 index 00000000000..cfed11dd4cf --- /dev/null +++ b/homeassistant/components/roon/translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "duplicate_entry": "\u042d\u0442\u043e\u0442 \u0445\u043e\u0441\u0442 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "link": { + "description": "\u041f\u043e\u0441\u043b\u0435 \u043d\u0430\u0436\u0430\u0442\u0438\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u00ab\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\u00bb \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Roon Core, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u00bb \u0438 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 HomeAssistant \u043d\u0430 \u0432\u043a\u043b\u0430\u0434\u043a\u0435 \u00ab\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u00bb.", + "title": "Roon" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Roon", + "title": "\u0421\u0435\u0440\u0432\u0435\u0440 Roon" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json new file mode 100644 index 00000000000..894da32c39c --- /dev/null +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "duplicate_entry": "\u8a72\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u3002", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "link": { + "description": "\u5fc5\u9808\u65bc Roon \u4e2d\u8a8d\u8b49 Home Assistant\u3002\u9ede\u9078\u50b3\u9001\u5f8c\u3001\u958b\u555f Roon Core \u61c9\u7528\u7a0b\u5f0f\u3001\u6253\u958b\u8a2d\u5b9a\u4e26\u65bc\u64f4\u5145\uff08Extensions\uff09\u4e2d\u555f\u7528 HomeAssistant\u3002", + "title": "\u65bc Roon \u4e2d\u8a8d\u8b49 HomeAssistant" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8acb\u8f38\u5165 Roon \u4f3a\u670d\u5668\u4e3b\u6a5f\u540d\u7a31\u6216 IP\u3002", + "title": "\u8a2d\u5b9a Roon \u4f3a\u670d\u5668" + } + } + }, + "title": "Roon" +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index 0e9abe80f82..43fa17aa315 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Ce t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 configur\u00e9.", "already_in_progress": "La configuration du t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 en cours.", - "auth_missing": "Home Assistant n'est pas authentifi\u00e9 pour se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung.", + "auth_missing": "Home Assistant n'est pas autoris\u00e9 \u00e0 se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung. Veuillez v\u00e9rifier les param\u00e8tres de votre t\u00e9l\u00e9viseur pour autoriser Home Assistant.", "not_successful": "Impossible de se connecter \u00e0 cet appareil Samsung TV.", "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge." }, diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index e0420ba74af..022dddcd00b 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -10,8 +10,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", - "title": "" + "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet." }, "user": { "data": { diff --git a/homeassistant/components/scene/translations/no.json b/homeassistant/components/scene/translations/no.json deleted file mode 100644 index d8a4c453015..00000000000 --- a/homeassistant/components/scene/translations/no.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "" -} \ No newline at end of file diff --git a/homeassistant/components/script/translations/no.json b/homeassistant/components/script/translations/no.json index 28122450085..6cace1e1570 100644 --- a/homeassistant/components/script/translations/no.json +++ b/homeassistant/components/script/translations/no.json @@ -4,6 +4,5 @@ "off": "Av", "on": "P\u00e5" } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index d80fd795698..b351aed3049 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "Nivell de bateria actual de {entity_name}", + "is_current": "Intensitat actual de {entity_name}", + "is_energy": "Energia actual de {entity_name}", "is_humidity": "Humitat actual de {entity_name}", "is_illuminance": "Il\u00b7luminaci\u00f3 actual de {entity_name}", "is_power": "Pot\u00e8ncia actual de {entity_name}", + "is_power_factor": "Factor de pot\u00e8ncia actual de {entity_name}", "is_pressure": "Pressi\u00f3 actual de {entity_name}", "is_signal_strength": "Pot\u00e8ncia de senyal actual de {entity_name}", "is_temperature": "Temperatura actual de {entity_name}", "is_timestamp": "Marca temporal actual de {entity_name}", - "is_value": "Valor actual de {entity_name}" + "is_value": "Valor actual de {entity_name}", + "is_voltage": "Voltatge actual de {entity_name}" }, "trigger_type": { "battery_level": "Canvia el nivell de bateria de {entity_name}", + "current": "Canvia la intensitat de {entity_name}", + "energy": "Canvia l'energia de {entity_name}", "humidity": "Canvia la humitat de {entity_name}", "illuminance": "Canvia la il\u00b7luminaci\u00f3 de {entity_name}", "power": "Canvia la pot\u00e8ncia de {entity_name}", + "power_factor": "Canvia el factor de pot\u00e8ncia de {entity_name}", "pressure": "Canvia la pressi\u00f3 de {entity_name}", "signal_strength": "Canvia la pot\u00e8ncia de senyal de {entity_name}", "temperature": "Canvia la temperatura de {entity_name}", "timestamp": "Canvia la marca temporal de {entity_name}", - "value": "Canvia el valor de {entity_name}" + "value": "Canvia el valor de {entity_name}", + "voltage": "Canvia el voltatge de {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index 69138fec001..ae0c32df574 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "Current {entity_name} battery level", + "is_current": "Current {entity_name} current", + "is_energy": "Current {entity_name} energy", "is_humidity": "Current {entity_name} humidity", "is_illuminance": "Current {entity_name} illuminance", "is_power": "Current {entity_name} power", + "is_power_factor": "Current {entity_name} power factor", "is_pressure": "Current {entity_name} pressure", "is_signal_strength": "Current {entity_name} signal strength", "is_temperature": "Current {entity_name} temperature", "is_timestamp": "Current {entity_name} timestamp", - "is_value": "Current {entity_name} value" + "is_value": "Current {entity_name} value", + "is_voltage": "Current {entity_name} voltage" }, "trigger_type": { "battery_level": "{entity_name} battery level changes", + "current": "{entity_name} current changes", + "energy": "{entity_name} energy changes", "humidity": "{entity_name} humidity changes", "illuminance": "{entity_name} illuminance changes", "power": "{entity_name} power changes", + "power_factor": "{entity_name} power factor changes", "pressure": "{entity_name} pressure changes", "signal_strength": "{entity_name} signal strength changes", "temperature": "{entity_name} temperature changes", "timestamp": "{entity_name} timestamp changes", - "value": "{entity_name} value changes" + "value": "{entity_name} value changes", + "voltage": "{entity_name} voltage changes" } }, "state": { diff --git a/homeassistant/components/sensor/translations/es-419.json b/homeassistant/components/sensor/translations/es-419.json index e724fe3a106..acf91a79104 100644 --- a/homeassistant/components/sensor/translations/es-419.json +++ b/homeassistant/components/sensor/translations/es-419.json @@ -10,11 +10,5 @@ "value": "{entity_name} cambios de valor" } }, - "state": { - "_": { - "off": "", - "on": "" - } - }, "title": "Sensor" } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index 0a6335f97a0..b2db3151abf 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "Nivel de bater\u00eda actual de {entity_name}", + "is_current": "Corriente actual de {entity_name}", + "is_energy": "Energ\u00eda actual de {entity_name}", "is_humidity": "Humedad actual de {entity_name}", "is_illuminance": "Luminosidad actual de {entity_name}", "is_power": "Potencia actual de {entity_name}", + "is_power_factor": "Factor de potencia actual de {entity_name}", "is_pressure": "Presi\u00f3n actual de {entity_name}", "is_signal_strength": "Intensidad de la se\u00f1al actual de {entity_name}", "is_temperature": "Temperatura actual de {entity_name}", "is_timestamp": "Marca de tiempo actual de {entity_name}", - "is_value": "Valor actual de {entity_name}" + "is_value": "Valor actual de {entity_name}", + "is_voltage": "Voltaje actual de {entity_name}" }, "trigger_type": { "battery_level": "Cambios de nivel de bater\u00eda de {entity_name}", + "current": "Cambio de corriente en {entity_name}", + "energy": "Cambio de energ\u00eda en {entity_name}", "humidity": "Cambios de humedad de {entity_name}", "illuminance": "Cambios de luminosidad de {entity_name}", "power": "Cambios de potencia de {entity_name}", + "power_factor": "Cambio de factor de potencia en {entity_name}", "pressure": "Cambios de presi\u00f3n de {entity_name}", "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name}", "temperature": "{entity_name} cambios de temperatura", "timestamp": "{entity_name} cambios de fecha y hora", - "value": "Cambios de valor de la {entity_name}" + "value": "Cambios de valor de la {entity_name}", + "voltage": "Cambio de voltaje en {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index e8cd7046231..84a8b2773a5 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "Livello della batteria attuale di {entity_name}", + "is_current": "Corrente attuale di {entity_name}", + "is_energy": "Energia attuale di {entity_name}", "is_humidity": "Umidit\u00e0 attuale di {entity_name}", "is_illuminance": "Illuminazione attuale di {entity_name}", "is_power": "Alimentazione attuale di {entity_name}", + "is_power_factor": "Fattore di potenza attuale di {entity_name}", "is_pressure": "Pressione attuale di {entity_name}", "is_signal_strength": "Potenza del segnale attuale di {entity_name}", "is_temperature": "Temperatura attuale di {entity_name}", "is_timestamp": "Data e ora attuali di {entity_name}", - "is_value": "Valore attuale di {entity_name}" + "is_value": "Valore attuale di {entity_name}", + "is_voltage": "Tensione attuale di {entity_name}" }, "trigger_type": { "battery_level": "variazioni del livello di batteria di {entity_name} ", + "current": "variazioni di corrente di {entity_name}", + "energy": "variazioni di energia di {entity_name}", "humidity": "variazioni di umidit\u00e0 di {entity_name} ", "illuminance": "variazioni dell'illuminazione di {entity_name}", "power": "variazioni di alimentazione di {entity_name}", + "power_factor": "variazioni del fattore di potenza di {entity_name}", "pressure": "variazioni della pressione di {entity_name}", "signal_strength": "variazioni della potenza del segnale di {entity_name}", "temperature": "variazioni di temperatura di {entity_name}", "timestamp": "variazioni di data e ora di {entity_name}", - "value": "{entity_name} valori cambiati" + "value": "{entity_name} valori cambiati", + "voltage": "variazioni di tensione di {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/lb.json b/homeassistant/components/sensor/translations/lb.json index e57edbb656b..97c0e2f0a6b 100644 --- a/homeassistant/components/sensor/translations/lb.json +++ b/homeassistant/components/sensor/translations/lb.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "Aktuell {entity_name} Batterie niveau", + "is_current": "Aktuell {entity_name} Stroum", + "is_energy": "Aktuell {entity_name} Energie", "is_humidity": "Aktuell {entity_name} Fiichtegkeet", "is_illuminance": "Aktuell {entity_name} Beliichtung", "is_power": "Aktuell {entity_name} Leeschtung", + "is_power_factor": "Aktuell {entity_name} Leeschtung Faktor", "is_pressure": "Aktuell {entity_name} Drock", "is_signal_strength": "Aktuell {entity_name} Signal St\u00e4erkt", "is_temperature": "Aktuell {entity_name} Temperatur", "is_timestamp": "Aktuelle {entity_name} Z\u00e4itstempel", - "is_value": "Aktuelle {entity_name} W\u00e4ert" + "is_value": "Aktuelle {entity_name} W\u00e4ert", + "is_voltage": "Aktuell {entity_name} Spannung" }, "trigger_type": { "battery_level": "{entity_name} Batterie niveau \u00e4nnert", + "current": "{entity_name} Stroum \u00e4nnert", + "energy": "{entity_name} Energie \u00e4nnert", "humidity": "{entity_name} Fiichtegkeet \u00e4nnert", "illuminance": "{entity_name} Beliichtung \u00e4nnert", "power": "{entity_name} Leeschtung \u00e4nnert", + "power_factor": "{entity_name} Leeschtung Faktor \u00e4nnert", "pressure": "{entity_name} Drock \u00e4nnert", "signal_strength": "{entity_name} Signal St\u00e4erkt \u00e4nnert", "temperature": "{entity_name} Temperatur \u00e4nnert", "timestamp": "{entity_name} Z\u00e4itstempel \u00e4nnert", - "value": "{entity_name} W\u00e4ert \u00e4nnert" + "value": "{entity_name} W\u00e4ert \u00e4nnert", + "voltage": "{entity_name} Spannung \u00e4nnert" } }, "state": { diff --git a/homeassistant/components/sensor/translations/nb.json b/homeassistant/components/sensor/translations/nb.json index 28122450085..6cace1e1570 100644 --- a/homeassistant/components/sensor/translations/nb.json +++ b/homeassistant/components/sensor/translations/nb.json @@ -4,6 +4,5 @@ "off": "Av", "on": "P\u00e5" } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index 80b6822607a..d8d05b81042 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "Gjeldende {entity_name} batteriniv\u00e5", + "is_current": "Gjeldende {entity_name} gjeldende", + "is_energy": "Gjeldende {entity_name} energi", "is_humidity": "Gjeldende {entity_name} fuktighet", "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", "is_power": "Gjeldende {entity_name}-effekt", + "is_power_factor": "Gjeldende {entity_name} effektfaktor", "is_pressure": "Gjeldende {entity_name} trykk", "is_signal_strength": "Gjeldende {entity_name} signalstyrke", "is_temperature": "Gjeldende {entity_name} temperatur", "is_timestamp": "Gjeldende {entity_name} tidsstempel", - "is_value": "Gjeldende {entity_name} verdi" + "is_value": "Gjeldende {entity_name} verdi", + "is_voltage": "Gjeldende spenning p\u00e5 {entity_name}" }, "trigger_type": { "battery_level": "{entity_name} batteriniv\u00e5 endres", + "current": "{entity_name} gjeldende endringer", + "energy": "{entity_name} energiendringer", "humidity": "{entity_name} fuktighets endringer", "illuminance": "{entity_name} belysningsstyrke endringer", "power": "{entity_name} effektendringer", + "power_factor": "{entity_name} effektfaktorendringer", "pressure": "{entity_name} trykk endringer", "signal_strength": "{entity_name} signalstyrkeendringer", "temperature": "{entity_name} temperaturendringer", "timestamp": "{entity_name} tidsstempel endringer", - "value": "{entity_name} verdi endringer" + "value": "{entity_name} verdi endringer", + "voltage": "{entity_name} spenningsendringer" } }, "state": { @@ -28,6 +36,5 @@ "off": "Av", "on": "P\u00e5" } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/pt-BR.json b/homeassistant/components/sensor/translations/pt-BR.json index 5da527abbab..337a4d9f318 100644 --- a/homeassistant/components/sensor/translations/pt-BR.json +++ b/homeassistant/components/sensor/translations/pt-BR.json @@ -1,4 +1,12 @@ { + "device_automation": { + "condition_type": { + "is_humidity": "Humidade atual do(a) {entity_name}", + "is_pressure": "Press\u00e3o atual do(a) {entity_name}", + "is_signal_strength": "For\u00e7a do sinal atual do(a) {entity_name}", + "is_temperature": "Temperatura atual do(a) {entity_name}" + } + }, "state": { "_": { "off": "Desligado", diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index 576affdf40d..ae84c843bc3 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_current": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", + "is_energy": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "is_humidity": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_illuminance": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_power": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_power_factor": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043a\u043e\u044d\u0444\u0444\u0438\u0446\u0438\u0435\u043d\u0442\u0430 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "is_pressure": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_signal_strength": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_temperature": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "is_timestamp": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", - "is_value": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" + "is_value": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_voltage": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, "trigger_type": { "battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "current": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", + "energy": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "illuminance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "power_factor": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u044d\u0444\u0444\u0438\u0446\u0438\u0435\u043d\u0442 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "pressure": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "signal_strength": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "temperature": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "timestamp": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", - "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" + "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "voltage": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" } }, "state": { diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index 20221a91d60..56350e501ef 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "\u76ee\u524d{entity_name}\u96fb\u91cf", + "is_current": "\u76ee\u524d{entity_name}\u96fb\u6d41", + "is_energy": "\u76ee\u524d{entity_name}\u96fb\u529b", "is_humidity": "\u76ee\u524d{entity_name}\u6fd5\u5ea6", "is_illuminance": "\u76ee\u524d{entity_name}\u7167\u5ea6", "is_power": "\u76ee\u524d{entity_name}\u96fb\u529b", + "is_power_factor": "\u76ee\u524d{entity_name}\u529f\u7387\u56e0\u6578", "is_pressure": "\u76ee\u524d{entity_name}\u58d3\u529b", "is_signal_strength": "\u76ee\u524d{entity_name}\u8a0a\u865f\u5f37\u5ea6", "is_temperature": "\u76ee\u524d{entity_name}\u6eab\u5ea6", "is_timestamp": "\u76ee\u524d{entity_name}\u6642\u9593\u6a19\u8a18", - "is_value": "\u76ee\u524d{entity_name}\u503c" + "is_value": "\u76ee\u524d{entity_name}\u503c", + "is_voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3" }, "trigger_type": { "battery_level": "{entity_name}\u96fb\u91cf\u8b8a\u66f4", + "current": "\u76ee\u524d{entity_name}\u96fb\u6d41\u8b8a\u66f4", + "energy": "\u76ee\u524d{entity_name}\u96fb\u529b\u8b8a\u66f4", "humidity": "{entity_name}\u6fd5\u5ea6\u8b8a\u66f4", "illuminance": "{entity_name}\u7167\u5ea6\u8b8a\u66f4", "power": "{entity_name}\u96fb\u529b\u8b8a\u66f4", + "power_factor": "\u76ee\u524d{entity_name}\u529f\u7387\u56e0\u6578\u8b8a\u66f4", "pressure": "{entity_name}\u58d3\u529b\u8b8a\u66f4", "signal_strength": "{entity_name}\u8a0a\u865f\u5f37\u5ea6\u8b8a\u66f4", "temperature": "{entity_name}\u6eab\u5ea6\u8b8a\u66f4", "timestamp": "{entity_name}\u6642\u9593\u6a19\u8a18\u8b8a\u66f4", - "value": "{entity_name}\u503c\u8b8a\u66f4" + "value": "{entity_name}\u503c\u8b8a\u66f4", + "voltage": "\u76ee\u524d{entity_name}\u96fb\u58d3\u8b8a\u66f4" } }, "state": { diff --git a/homeassistant/components/sentry/translations/ca.json b/homeassistant/components/sentry/translations/ca.json index 43302714b97..7b24dba7c77 100644 --- a/homeassistant/components/sentry/translations/ca.json +++ b/homeassistant/components/sentry/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Sentry ja est\u00e0 configurat" + "already_configured": "Sentry ja est\u00e0 configurat", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { "bad_dsn": "DSN inv\u00e0lid", @@ -9,9 +10,28 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Introdueix el DSN de Sentry", "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Nom opcional de l'entorn.", + "event_custom_components": "Envia esdeveniments de components personalitzats", + "event_handled": "Envia esdeveniments gestionats", + "event_third_party_packages": "Envia esdeveniments de tercers", + "logging_event_level": "El nivell (log level) en que Sentry registrar\u00e0 un esdeveniment", + "logging_level": "El nivell (log level) en que Sentry registrar\u00e0 en miques", + "tracing": "Activa el seguiment de rendiment", + "tracing_sample_rate": "Freq\u00fc\u00e8ncia de mostreig del rastreig; entre 0.0 i 1.0 (1.0 = 100%)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/en.json b/homeassistant/components/sentry/translations/en.json index 13387488ea6..9ffeb30000b 100644 --- a/homeassistant/components/sentry/translations/en.json +++ b/homeassistant/components/sentry/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Sentry is already configured" + "already_configured": "Sentry is already configured", + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "bad_dsn": "Invalid DSN", @@ -9,9 +10,28 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Enter your Sentry DSN", "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Optional name of the environment.", + "event_custom_components": "Send events from custom components", + "event_handled": "Send handled events", + "event_third_party_packages": "Send events from third-party packages", + "logging_event_level": "The log level Sentry will register an event for", + "logging_level": "The log level Sentry will record logs as breadcrums for", + "tracing": "Enable performance tracing", + "tracing_sample_rate": "Tracing sample rate; between 0.0 and 1.0 (1.0 = 100%)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/es.json b/homeassistant/components/sentry/translations/es.json index 445d7566240..47caae6c541 100644 --- a/homeassistant/components/sentry/translations/es.json +++ b/homeassistant/components/sentry/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Sentry ya est\u00e1 configurado" + "already_configured": "Sentry ya est\u00e1 configurado", + "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "bad_dsn": "DSN no v\u00e1lido", @@ -9,9 +10,28 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Introduzca su DSN Sentry", "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Nombre opcional del entorno.", + "event_custom_components": "Env\u00eda eventos desde componentes personalizados", + "event_handled": "Enviar eventos controlados", + "event_third_party_packages": "Env\u00eda eventos desde paquetes de terceros", + "logging_event_level": "El nivel de registro Sentry registrar\u00e1 un evento para", + "logging_level": "El nivel de registro Sentry registrar\u00e1 registros como migas de pan para", + "tracing": "Habilitar el seguimiento del rendimiento", + "tracing_sample_rate": "Seguimiento de la frecuencia de muestreo; entre 0.0 y 1.0 (1.0 = 100%)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/fr.json b/homeassistant/components/sentry/translations/fr.json index fce57ec4b9c..933bc8ad7fb 100644 --- a/homeassistant/components/sentry/translations/fr.json +++ b/homeassistant/components/sentry/translations/fr.json @@ -9,9 +9,26 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Entrez votre DSN Sentry", "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Nom facultatif de l'environnement.", + "event_custom_components": "Envoyer des \u00e9v\u00e9nements \u00e0 partir de composants personnalis\u00e9s", + "event_handled": "Envoyer des \u00e9v\u00e9nements g\u00e9r\u00e9s", + "event_third_party_packages": "Envoyer des \u00e9v\u00e9nements \u00e0 partir de paquets de tiers", + "logging_event_level": "Le niveau de journal de Sentry enregistrera un \u00e9v\u00e9nement pour", + "tracing": "Activer le suivi des performances" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/it.json b/homeassistant/components/sentry/translations/it.json index 44090de0b62..2f022129022 100644 --- a/homeassistant/components/sentry/translations/it.json +++ b/homeassistant/components/sentry/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Sentry \u00e8 gi\u00e0 configurato" + "already_configured": "Sentry \u00e8 gi\u00e0 configurato", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { "bad_dsn": "DSN non valido", @@ -9,9 +10,28 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Inserisci il tuo DSN Sentry", "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Nome opzionale dell'ambiente.", + "event_custom_components": "Inviare eventi da componenti personalizzati", + "event_handled": "Inviare eventi gestiti", + "event_third_party_packages": "Inviare eventi da pacchetti di terze parti", + "logging_event_level": "Il livello di registro Sentry registrer\u00e0 un evento per", + "logging_level": "Il livello di registro Sentry registrer\u00e0 i registri cos\u00ec granulari per", + "tracing": "Attivare il tracciamento delle prestazioni", + "tracing_sample_rate": "Frequenza di campionamento del tracciamento; tra 0,0 e 1,0 (1,0 = 100%)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/lb.json b/homeassistant/components/sentry/translations/lb.json index 92baa5502d7..169461e52f7 100644 --- a/homeassistant/components/sentry/translations/lb.json +++ b/homeassistant/components/sentry/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Sentry ass scho konfigur\u00e9iert" + "already_configured": "Sentry ass scho konfigur\u00e9iert", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." }, "error": { "bad_dsn": "Ong\u00eblteg DSN", @@ -9,9 +10,26 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "Gitt \u00e4r Sentry DSN un", "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Optionelle Numm vum Environnement", + "event_custom_components": "Sch\u00e9ck Evenementer aus personalis\u00e9ierten Komponenten", + "event_handled": "Sch\u00e9ck ger\u00e9iert Evenementer", + "event_third_party_packages": "Sch\u00e9ck Evenementer vun Dr\u00ebtt Partei Packagen", + "tracing": "Aktiv\u00e9ier Leeschtung Tracing", + "tracing_sample_rate": "Echantillon erm\u00ebttelen; t\u00ebschent 0,0 an 1.0" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/no.json b/homeassistant/components/sentry/translations/no.json index 26a469ce341..48504931104 100644 --- a/homeassistant/components/sentry/translations/no.json +++ b/homeassistant/components/sentry/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Sentry er allerede konfigurert" + "already_configured": "Sentry er allerede konfigurert", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { "bad_dsn": "Ugyldig datakildenavn (DSN)", @@ -9,8 +10,26 @@ }, "step": { "user": { - "description": "Fyll inn din Sentry DNS", - "title": "" + "data": { + "dsn": "DSN" + }, + "description": "Fyll inn din Sentry DNS" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Valgfritt navn p\u00e5 milj\u00f8et.", + "event_custom_components": "Send hendelser fra egendefinerte komponenter", + "event_handled": "Send h\u00e5ndterte hendelser", + "event_third_party_packages": "Send hendelser fra tredjepartspakker", + "logging_event_level": "Loggniv\u00e5et Sentry vil registrere en hendelse for", + "logging_level": "Loggniv\u00e5et Sentry vil registrere logger som br\u00f8dsmuler for", + "tracing": "Aktivere ytelsessporing", + "tracing_sample_rate": "Sporing av samplingsfrekvens; mellom 0,0 og 1,0 (1,0 = 100 %)" + } } } } diff --git a/homeassistant/components/sentry/translations/ru.json b/homeassistant/components/sentry/translations/ru.json index 29ae44ca9eb..ab8023e505c 100644 --- a/homeassistant/components/sentry/translations/ru.json +++ b/homeassistant/components/sentry/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { "bad_dsn": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 DSN.", @@ -9,9 +10,28 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448 DSN Sentry", "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "event_custom_components": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0438\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432", + "event_handled": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f", + "event_third_party_packages": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0438\u0437 \u0441\u0442\u043e\u0440\u043e\u043d\u043d\u0438\u0445 \u043f\u0430\u043a\u0435\u0442\u043e\u0432", + "logging_event_level": "\u0417\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0436\u0443\u0440\u043d\u0430\u043b\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439", + "logging_level": "\u0417\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0436\u0443\u0440\u043d\u0430\u043b\u044b \u0432 \u0432\u0438\u0434\u0435 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0446\u0435\u043f\u043e\u0447\u0435\u043a", + "tracing": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438", + "tracing_sample_rate": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u0434\u0438\u0441\u043a\u0440\u0435\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0442\u0440\u0430\u0441\u0441\u0438\u0440\u043e\u0432\u043a\u0438; \u043e\u0442 0,0 \u0434\u043e 1,0 (1,0 = 100%)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/zh-Hant.json b/homeassistant/components/sentry/translations/zh-Hant.json index 277bff66dcb..5c77442a03e 100644 --- a/homeassistant/components/sentry/translations/zh-Hant.json +++ b/homeassistant/components/sentry/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Sentry \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "Sentry \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" }, "error": { "bad_dsn": "DSN \u7121\u6548", @@ -9,9 +10,28 @@ }, "step": { "user": { + "data": { + "dsn": "DSN" + }, "description": "\u8f38\u5165 Sentry DSN", "title": "Sentry" } } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "\u74b0\u5883\u9078\u9805\u540d\u7a31", + "event_custom_components": "\u50b3\u9001\u81ea\u8a02\u5143\u4ef6\u4e8b\u4ef6", + "event_handled": "\u50b3\u9001\u5df2\u8655\u7406\u4e8b\u4ef6", + "event_third_party_packages": "\u50b3\u9001\u7b2c\u4e09\u65b9\u5c01\u5305\u4e8b\u4ef6", + "logging_event_level": "\u65e5\u8a8c\u7b49\u7d1a\u76e3\u63a7\u5c07\u6703\u8a3b\u518a\u4e8b\u4ef6\u70ba", + "logging_level": "\u65e5\u8a8c\u7b49\u7d1a\u76e3\u63a7\u5c07\u6703\u7d00\u9304\u4e8b\u4ef6\u70ba\u6a94\u6848\u5c0e\u822a\u70ba", + "tracing": "\u958b\u555f\u6548\u80fd\u8ffd\u8e64", + "tracing_sample_rate": "\u8ffd\u8e64\u63a1\u6a23\u7bc4\u570d\uff0c\u4ecb\u65bc 0.0 \u53ca 1.0 \u4e4b\u9593\uff081.0 = 100%\uff09" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json new file mode 100644 index 00000000000..56e2e816fa3 --- /dev/null +++ b/homeassistant/components/shelly/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "auth_not_supported": "Actualment els dispositius Shelly amb autenticaci\u00f3 no son compatibles.", + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "Voleu configurar {model} a {host}?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index 89660bbda1a..ebcb1976516 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "Device is already configured" }, "error": { - "auth_not_supported": "Authenticated Shelly devices are not currently supported.", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "auth_not_supported": "Shelly devices requiring authentication are not currently supported.", + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" }, "flow_title": "Shelly: {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "[%key:common::config_flow::data::host%]" + "host": "Host" } } } diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json new file mode 100644 index 00000000000..fa687f586d3 --- /dev/null +++ b/homeassistant/components/shelly/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "auth_not_supported": "Los dispositivos Shelly que requieren autenticaci\u00f3n no son compatibles actualmente.", + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "\u00bfQuieres configurar el {model} en {host}?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json new file mode 100644 index 00000000000..5c2d2ed5c7d --- /dev/null +++ b/homeassistant/components/shelly/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "flow_title": "Shelly: {name}", + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json new file mode 100644 index 00000000000..b74d00a2cb2 --- /dev/null +++ b/homeassistant/components/shelly/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "auth_not_supported": "I dispositivi Shelly che richiedono l'autenticazione non sono attualmente supportati.", + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "Vuoi impostare {model} su {host}?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/lb.json b/homeassistant/components/shelly/translations/lb.json new file mode 100644 index 00000000000..b50f528c3c0 --- /dev/null +++ b/homeassistant/components/shelly/translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "auth_not_supported": "Shelly Apparaten d\u00e9i eng Authentifikatioun ben\u00e9idegen ginn aktuell net \u00ebnnerst\u00ebtzt.", + "cannot_connect": "Feeler beim verbannen", + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "Soll de {model} um {host} konfigur\u00e9iert ginn?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json new file mode 100644 index 00000000000..8f3b684abc1 --- /dev/null +++ b/homeassistant/components/shelly/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "auth_not_supported": "Shelly-enheter som krever godkjenning st\u00f8ttes for \u00f8yeblikket ikke.", + "cannot_connect": "Tilkobling mislyktes.", + "unknown": "Uventet feil" + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "Vil du konfigurere {model} p\u00e5 {host}?" + }, + "user": { + "data": { + "host": "Vert" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json new file mode 100644 index 00000000000..1bf40fec860 --- /dev/null +++ b/homeassistant/components/shelly/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + }, + "error": { + "auth_not_supported": "Urz\u0105dzenia Shelly wymagaj\u0105ce uwierzytelnienia nie s\u0105 obecnie obs\u0142ugiwane.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json new file mode 100644 index 00000000000..f265a49b6d4 --- /dev/null +++ b/homeassistant/components/shelly/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "auth_not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Shelly, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0438\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c {model} ({host}) ?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json new file mode 100644 index 00000000000..ab6dec3cd2d --- /dev/null +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "auth_not_supported": "\u76ee\u524d\u4e0d\u652f\u63f4 Shelly \u8a2d\u5099\u6240\u9700\u8a8d\u8b49\u3002", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Shelly\uff1a{name}", + "step": { + "confirm_discovery": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model}\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 98727fe4c57..bdfd5d76198 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -12,7 +12,7 @@ }, "step": { "mfa": { - "description": "Consulta el correu i busca-hi un missatge amb un enlla\u00e7 de SimpliSafe. Despr\u00e9s de verificar l'enlla\u00e7, torneu aqu\u00ed per completar la instal\u00b7laci\u00f3 de la integraci\u00f3.", + "description": "Consulta el correu i busca-hi un missatge amb un enlla\u00e7 de SimpliSafe. Despr\u00e9s de verificar l'enlla\u00e7, torna aqu\u00ed per completar la instal\u00b7laci\u00f3 de la integraci\u00f3.", "title": "Autenticaci\u00f3 multi-factor SimpliSafe" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 64000e66d62..0c177fd02ff 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso.", - "reauth_successful": "SimpliSafe se ha reautenticado correctamente." + "reauth_successful": "SimpliSafe se volvi\u00f3 a autenticar con \u00e9xito." }, "error": { "identifier_exists": "Cuenta ya registrada", diff --git a/homeassistant/components/smappee/translations/ca.json b/homeassistant/components/smappee/translations/ca.json index 569c76518c2..54a781c1f4a 100644 --- a/homeassistant/components/smappee/translations/ca.json +++ b/homeassistant/components/smappee/translations/ca.json @@ -1,12 +1,33 @@ { "config": { "abort": { + "already_configured_device": "El dispositiu ja est\u00e0 configurat", + "already_configured_local_device": "Dispositiu(s) local ja configurat. Elimina'ls primer abans de configurar un dispositiu al n\u00favol.", "authorize_url_timeout": "Temps d'espera esgotat generant l'URL d'autoritzaci\u00f3.", + "connection_error": "No s'ha pogut connectar amb el Smappee.", + "invalid_mdns": "Dispositiu no compatible amb la integraci\u00f3 de Smappee.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." }, + "flow_title": "Smappee: {name}", "step": { + "environment": { + "data": { + "environment": "Entorn" + }, + "description": "Configura el teu Smappee per a integrar-lo amb Home Assistant." + }, + "local": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Introdueix l'amfitri\u00f3 per iniciar la integraci\u00f3 local de Smappee" + }, "pick_implementation": { "title": "Selecciona un m\u00e8tode d'autenticaci\u00f3" + }, + "zeroconf_confirm": { + "description": "Vols afegir el dispositiu Smappee amb n\u00famero de s\u00e8rie `{serialnumber}` a Home Assistant?", + "title": "Dispositiu Smappee descobert" } } } diff --git a/homeassistant/components/smappee/translations/en.json b/homeassistant/components/smappee/translations/en.json index 36d8d3a8f35..57f1498d5fa 100644 --- a/homeassistant/components/smappee/translations/en.json +++ b/homeassistant/components/smappee/translations/en.json @@ -1,34 +1,34 @@ { "config": { + "abort": { + "already_configured_device": "Device is already configured", + "already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.", + "authorize_url_timeout": "Timeout generating authorize url.", + "connection_error": "Failed to connect to Smappee device.", + "invalid_mdns": "Unsupported device for the Smappee integration.", + "missing_configuration": "The component is not configured. Please follow the documentation." + }, "flow_title": "Smappee: {name}", "step": { "environment": { - "description": "Set up your Smappee to integrate with Home Assistant.", "data": { "environment": "Environment" - } + }, + "description": "Set up your Smappee to integrate with Home Assistant." }, "local": { - "description": "Enter the host to initiate the Smappee local integration", "data": { "host": "Host" - } + }, + "description": "Enter the host to initiate the Smappee local integration" + }, + "pick_implementation": { + "title": "Pick Authentication Method" }, "zeroconf_confirm": { "description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?", "title": "Discovered Smappee device" - }, - "pick_implementation": { - "title": "Pick Authentication Method" } - }, - "abort": { - "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", - "already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.", - "authorize_url_timeout": "Timeout generating authorize url.", - "connection_error": "Failed to connect to Smappee device.", - "missing_configuration": "The component is not configured. Please follow the documentation.", - "invalid_mdns": "Unsupported device for the Smappee integration." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index f67676e5165..543c988b356 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -1,12 +1,33 @@ { "config": { "abort": { + "already_configured_device": "El dispositivo ya est\u00e1 configurado", + "already_configured_local_device": "Los dispositivos locales ya est\u00e1n configurados. Elim\u00ednelos primero antes de configurar un dispositivo en la nube.", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "connection_error": "No se pudo conectar al dispositivo Smappee.", + "invalid_mdns": "Dispositivo no compatible para la integraci\u00f3n de Smappee.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n." }, + "flow_title": "Smappee: {name}", "step": { + "environment": { + "data": { + "environment": "Ambiente" + }, + "description": "Configura tu Smappee para que se integre con Home Assistant." + }, + "local": { + "data": { + "host": "Host" + }, + "description": "Ingrese el host para iniciar la integraci\u00f3n local de Smappee" + }, "pick_implementation": { "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + }, + "zeroconf_confirm": { + "description": "\u00bfDesea agregar el dispositivo Smappee con el n\u00famero de serie `{serialnumber}` a Home Assistant?", + "title": "Dispositivo Smappee descubierto" } } } diff --git a/homeassistant/components/smappee/translations/it.json b/homeassistant/components/smappee/translations/it.json index 057bce6b714..66994517c2f 100644 --- a/homeassistant/components/smappee/translations/it.json +++ b/homeassistant/components/smappee/translations/it.json @@ -1,12 +1,33 @@ { "config": { "abort": { + "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_configured_local_device": "L'apparecchio o gli apparecchi locali sono gi\u00e0 configurati. Si prega di rimuoverli prima di configurare un dispositivo cloud.", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "connection_error": "Impossibile connettersi al dispositivo Smappee.", + "invalid_mdns": "Dispositivo non supportato per l'integrazione Smappee.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." }, + "flow_title": "Smappee: {name}", "step": { + "environment": { + "data": { + "environment": "Ambiente" + }, + "description": "Configura il tuo Smappee per integrarlo con Home Assistant." + }, + "local": { + "data": { + "host": "Host" + }, + "description": "Immettere l'host per avviare l'integrazione locale di Smappee" + }, "pick_implementation": { "title": "Scegliere il metodo di autenticazione" + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere il dispositivo Smappee con numero di serie `{serialnumber}` a Home Assistant?", + "title": "Dispositivo Smappee rilevato" } } } diff --git a/homeassistant/components/smappee/translations/lb.json b/homeassistant/components/smappee/translations/lb.json index 8169e17a6de..7f514644918 100644 --- a/homeassistant/components/smappee/translations/lb.json +++ b/homeassistant/components/smappee/translations/lb.json @@ -1,12 +1,33 @@ { "config": { "abort": { + "already_configured_device": "Apparat ass scho konfigur\u00e9iert", + "already_configured_local_device": "Lokalen Apparat ass scho konfigur\u00e9iert. L\u00e4sch d\u00e9i iers de ee Cloud Apparat dob\u00e4isetz.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", + "connection_error": "Feeler beim verbannen mam Smappee Apparat.", + "invalid_mdns": "Net \u00ebnnerst\u00ebtzten Apparat fir Smappee Integratioun.", "missing_configuration": "Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." }, + "flow_title": "Smappee: {name}", "step": { + "environment": { + "data": { + "environment": "\u00cbmwelt" + }, + "description": "Smappee ariichten fir d'Integratioun mam Home Assistant." + }, + "local": { + "data": { + "host": "Host" + }, + "description": "G\u00ebff den Host un fir Smappee lokal Integratioun z'initialis\u00e9ieren" + }, "pick_implementation": { "title": "Wiel Authentifikatiouns Method aus" + }, + "zeroconf_confirm": { + "description": "Soll den Smappee Apparat mat der Seriennummer `{serialnumber}` am Home Assistant dob\u00e4igesat ginn?", + "title": "Entdeckten Smappee Apparat" } } } diff --git a/homeassistant/components/smappee/translations/no.json b/homeassistant/components/smappee/translations/no.json index 6b2141fd61e..76e96614aa6 100644 --- a/homeassistant/components/smappee/translations/no.json +++ b/homeassistant/components/smappee/translations/no.json @@ -1,12 +1,33 @@ { "config": { "abort": { + "already_configured_device": "Enheten er allerede konfigurert", + "already_configured_local_device": "Lokale enheter er allerede konfigurert. Fjern de f\u00f8rst f\u00f8r du konfigurerer en skyenhet.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "connection_error": "Kunne ikke koble til Smappee-enheten.", + "invalid_mdns": "Ikke-st\u00f8ttet enhet for Smappee-integrasjonen.", "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." }, + "flow_title": "Smappee: {navn}", "step": { + "environment": { + "data": { + "environment": "Milj\u00f8" + }, + "description": "Konfigurer Smappee til \u00e5 integreres med Home Assistant." + }, + "local": { + "data": { + "host": "Vert" + }, + "description": "Angi verten for \u00e5 starte den lokale Smappee-integreringen" + }, "pick_implementation": { "title": "Velg godkjenningsmetode" + }, + "zeroconf_confirm": { + "description": "Vil du legge Smappee-enheten med serienummer ` {serialnumber} ` til Home Assistant?", + "title": "Oppdaget Smappee-enhet" } } } diff --git a/homeassistant/components/smappee/translations/pt-BR.json b/homeassistant/components/smappee/translations/pt-BR.json new file mode 100644 index 00000000000..cbaae7d2d6a --- /dev/null +++ b/homeassistant/components/smappee/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured_device": "Dispositivo j\u00e1 configurado" + }, + "step": { + "local": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/ru.json b/homeassistant/components/smappee/translations/ru.json index 101e8d2fcec..b3650a483f6 100644 --- a/homeassistant/components/smappee/translations/ru.json +++ b/homeassistant/components/smappee/translations/ru.json @@ -1,12 +1,33 @@ { "config": { "abort": { + "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_configured_local_device": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0434\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0438\u0445 \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "invalid_mdns": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", "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." }, + "flow_title": "Smappee: {name}", "step": { + "environment": { + "data": { + "environment": "\u041e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435" + }, + "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 Smappee." + }, + "local": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0447\u0430\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0441 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Smappee" + }, "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Smappee \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serialnumber}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Smappee" } } } diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json index 8a93907cbb4..7636ea5b34b 100644 --- a/homeassistant/components/smappee/translations/zh-Hant.json +++ b/homeassistant/components/smappee/translations/zh-Hant.json @@ -1,12 +1,33 @@ { "config": { "abort": { + "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_local_device": "\u672c\u5730\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\uff0c\u8acb\u5148\u9032\u884c\u79fb\u9664\u5f8c\u518d\u8a2d\u5b9a\u96f2\u7aef\u8a2d\u5099\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "connection_error": "Smappee \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002", + "invalid_mdns": "Smappee \u6574\u5408\u4e0d\u652f\u63f4\u7684\u8a2d\u5099\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, + "flow_title": "Smappee\uff1a{name}", "step": { + "environment": { + "data": { + "environment": "\u74b0\u5883" + }, + "description": "\u8a2d\u5b9a Smappee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" + }, + "local": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8f38\u5165 Smappee \u672c\u5730\u6574\u5408\u4e3b\u6a5f\u7aef\u4ee5\u958b\u59cb" + }, "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Smappee \u8a2d\u5099\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Smappee \u8a2d\u5099" } } } diff --git a/homeassistant/components/smart_meter_texas/translations/ca.json b/homeassistant/components/smart_meter_texas/translations/ca.json new file mode 100644 index 00000000000..b2c1f34e1e4 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Proporciona el nom d'usuari i contrasenya per a Smart Meter Texas." + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/en.json b/homeassistant/components/smart_meter_texas/translations/en.json new file mode 100644 index 00000000000..dec29957514 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Provide your username and password for Smart Meter Texas." + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/es.json b/homeassistant/components/smart_meter_texas/translations/es.json new file mode 100644 index 00000000000..0423fee406b --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Proporciona tu nombre de usuario y contrase\u00f1a para Smart Meter Texas." + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/fr.json b/homeassistant/components/smart_meter_texas/translations/fr.json new file mode 100644 index 00000000000..e059820f962 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/fr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Fournissez votre nom d\u2019utilisateur et votre mot de passe pour Smart Meter Texas." + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/it.json b/homeassistant/components/smart_meter_texas/translations/it.json new file mode 100644 index 00000000000..787dfd6b8c2 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Fornisci il tuo nome utente e la password per Smart Meter Texas." + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/ko.json b/homeassistant/components/smart_meter_texas/translations/ko.json new file mode 100644 index 00000000000..444e3cf4463 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\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", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "Smart Meter Texas \uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/lb.json b/homeassistant/components/smart_meter_texas/translations/lb.json new file mode 100644 index 00000000000..8b60dd41c8c --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "description": "G\u00ebff den Benotzernumm an d'Passwuert fir den Smart Meter Texas un." + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/no.json b/homeassistant/components/smart_meter_texas/translations/no.json new file mode 100644 index 00000000000..693c93376d8 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes.", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Oppgi brukernavn og passord for Smart Meter Texas." + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/ru.json b/homeassistant/components/smart_meter_texas/translations/ru.json new file mode 100644 index 00000000000..2015377048d --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "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 Smart Meter Texas." + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/zh-Hant.json b/homeassistant/components/smart_meter_texas/translations/zh-Hant.json new file mode 100644 index 00000000000..a268bee7cc3 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u8f38\u5165 Smart Meter Texas \u5e33\u865f\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002" + } + } + }, + "title": "Smart Meter Texas" +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/ru.json b/homeassistant/components/sms/translations/ru.json index 85a99a37528..2200b582123 100644 --- a/homeassistant/components/sms/translations/ru.json +++ b/homeassistant/components/sms/translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/soma/translations/no.json b/homeassistant/components/soma/translations/no.json index 5c2c01ca7a6..5a084433eed 100644 --- a/homeassistant/components/soma/translations/no.json +++ b/homeassistant/components/soma/translations/no.json @@ -13,11 +13,9 @@ "step": { "user": { "data": { - "host": "Vert", - "port": "" + "host": "Vert" }, - "description": "Vennligst fyll inn tilkoblingsinnstillingene for din SOMA Connect.", - "title": "" + "description": "Vennligst fyll inn tilkoblingsinnstillingene for din SOMA Connect." } } } diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json index 694565b72d6..2a7ede2964a 100644 --- a/homeassistant/components/sonarr/translations/no.json +++ b/homeassistant/components/sonarr/translations/no.json @@ -8,14 +8,12 @@ "cannot_connect": "Tilkobling mislyktes.", "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "", "step": { "user": { "data": { "api_key": "API N\u00f8kkel", "base_path": "Bane til API", "host": "Vert", - "port": "", "ssl": "Sonarr bruker et SSL-sertifikat", "verify_ssl": "Sonarr bruker et riktig sertifikat" }, @@ -32,6 +30,5 @@ } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json index d7ffd002981..158a3d59391 100644 --- a/homeassistant/components/sonarr/translations/ru.json +++ b/homeassistant/components/sonarr/translations/ru.json @@ -5,7 +5,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." }, "flow_title": "Sonarr: {name}", diff --git a/homeassistant/components/songpal/translations/no.json b/homeassistant/components/songpal/translations/no.json index eb07eeb2daa..1096e9a0e89 100644 --- a/homeassistant/components/songpal/translations/no.json +++ b/homeassistant/components/songpal/translations/no.json @@ -7,7 +7,6 @@ "error": { "cannot_connect": "Tilkobling mislyktes." }, - "flow_title": "", "step": { "init": { "description": "Vil du sette opp {name} ({host})?" diff --git a/homeassistant/components/songpal/translations/ru.json b/homeassistant/components/songpal/translations/ru.json index 9bd54b442bc..b572236b7f7 100644 --- a/homeassistant/components/songpal/translations/ru.json +++ b/homeassistant/components/songpal/translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "not_songpal_device": "\u041d\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Songpal." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "flow_title": "Sony Songpal {name} ({host})", "step": { diff --git a/homeassistant/components/spider/translations/ca.json b/homeassistant/components/spider/translations/ca.json index 41d82da80cf..2713c58a85c 100644 --- a/homeassistant/components/spider/translations/ca.json +++ b/homeassistant/components/spider/translations/ca.json @@ -12,7 +12,8 @@ "data": { "password": "Contrasenya", "username": "Nom d'usuari" - } + }, + "title": "Inici de sessi\u00f3 amb un compte de mijn.ithodaalderop.nl" } } } diff --git a/homeassistant/components/spider/translations/it.json b/homeassistant/components/spider/translations/it.json new file mode 100644 index 00000000000..839d7a62a31 --- /dev/null +++ b/homeassistant/components/spider/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Accedi con l'account mijn.ithodaalderop.nl" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/lb.json b/homeassistant/components/spider/translations/lb.json new file mode 100644 index 00000000000..5d83fc8fc16 --- /dev/null +++ b/homeassistant/components/spider/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "error": { + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "title": "Mam mijn.ithodaalderop.nl Kont verbannen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/zh-Hant.json b/homeassistant/components/spider/translations/zh-Hant.json new file mode 100644 index 00000000000..96b9ad519d8 --- /dev/null +++ b/homeassistant/components/spider/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u4ee5 mijn.ithodaalderop.nl \u5e33\u865f\u767b\u5165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/no.json b/homeassistant/components/squeezebox/translations/no.json index ddda0b61be2..dd9192cfb50 100644 --- a/homeassistant/components/squeezebox/translations/no.json +++ b/homeassistant/components/squeezebox/translations/no.json @@ -10,13 +10,11 @@ "no_server_found": "Kan ikke automatisk oppdage serveren.", "unknown": "Uventet feil" }, - "flow_title": "", "step": { "edit": { "data": { "host": "Vert", "password": "Passord", - "port": "", "username": "Brukernavn" }, "title": "Redigere tilkoblingsinformasjon" @@ -28,6 +26,5 @@ "title": "Konfigurer Logitech Media Server" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/ru.json b/homeassistant/components/squeezebox/translations/ru.json index f12f6bb5e83..8b6c6a71ff6 100644 --- a/homeassistant/components/squeezebox/translations/ru.json +++ b/homeassistant/components/squeezebox/translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "no_server_found": "\u0421\u0435\u0440\u0432\u0435\u0440 LMS \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "no_server_found": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/starline/translations/fr.json b/homeassistant/components/starline/translations/fr.json index 3720cf10211..01385b7feee 100644 --- a/homeassistant/components/starline/translations/fr.json +++ b/homeassistant/components/starline/translations/fr.json @@ -11,7 +11,7 @@ "app_id": "ID de l'application", "app_secret": "Secret" }, - "description": "ID applicatif et code secret du compte d\u00e9veloppeur StarLine", + "description": "ID applicatif et code secret du [compte d\u00e9veloppeur StarLine](https://my.starline.ru/developer)", "title": "Informations d'identification de l'application" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/no.json b/homeassistant/components/starline/translations/no.json index 36545f3efd7..89dc882cf82 100644 --- a/homeassistant/components/starline/translations/no.json +++ b/homeassistant/components/starline/translations/no.json @@ -17,9 +17,7 @@ "auth_captcha": { "data": { "captcha_code": "Kode fra bilde" - }, - "description": "", - "title": "" + } }, "auth_mfa": { "data": { diff --git a/homeassistant/components/starline/translations/pt-BR.json b/homeassistant/components/starline/translations/pt-BR.json index 158c2b01cf9..3b73793804a 100644 --- a/homeassistant/components/starline/translations/pt-BR.json +++ b/homeassistant/components/starline/translations/pt-BR.json @@ -23,7 +23,8 @@ }, "auth_user": { "data": { - "password": "Senha" + "password": "Senha", + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/switch/translations/es-419.json b/homeassistant/components/switch/translations/es-419.json index 7fb04127b15..a7087a1bbf1 100644 --- a/homeassistant/components/switch/translations/es-419.json +++ b/homeassistant/components/switch/translations/es-419.json @@ -14,11 +14,5 @@ "turned_on": "{entity_name} encendido" } }, - "state": { - "_": { - "off": "", - "on": "" - } - }, "title": "Interruptor" } \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/ru.json b/homeassistant/components/syncthru/translations/ru.json index c3b88690a91..3e904f3d32a 100644 --- a/homeassistant/components/syncthru/translations/ru.json +++ b/homeassistant/components/syncthru/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { "invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index f8d7add4dc2..2bfc824cb76 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -21,22 +21,18 @@ "link": { "data": { "password": "Passord", - "port": "", "ssl": "Bruk SSL/TLS til \u00e5 koble til NAS-en", "username": "Brukernavn" }, - "description": "Vil du konfigurere {name} ({host})?", - "title": "" + "description": "Vil du konfigurere {name} ({host})?" }, "user": { "data": { "host": "Vert", "password": "Passord", - "port": "", "ssl": "Bruk SSL/TLS til \u00e5 koble til NAS-en", "username": "Brukernavn" - }, - "title": "" + } } } }, diff --git a/homeassistant/components/tag/translations/ca.json b/homeassistant/components/tag/translations/ca.json new file mode 100644 index 00000000000..fdac700612d --- /dev/null +++ b/homeassistant/components/tag/translations/ca.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/es.json b/homeassistant/components/tag/translations/es.json new file mode 100644 index 00000000000..6f21d3e8d6d --- /dev/null +++ b/homeassistant/components/tag/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "Etiqueta" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/fr.json b/homeassistant/components/tag/translations/fr.json new file mode 100644 index 00000000000..93ee415d38b --- /dev/null +++ b/homeassistant/components/tag/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "Balise" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/it.json b/homeassistant/components/tag/translations/it.json new file mode 100644 index 00000000000..a7622fae02b --- /dev/null +++ b/homeassistant/components/tag/translations/it.json @@ -0,0 +1,3 @@ +{ + "title": "Etichetta" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/lb.json b/homeassistant/components/tag/translations/lb.json new file mode 100644 index 00000000000..fdac700612d --- /dev/null +++ b/homeassistant/components/tag/translations/lb.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/no.json b/homeassistant/components/tag/translations/no.json new file mode 100644 index 00000000000..fdac700612d --- /dev/null +++ b/homeassistant/components/tag/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/pt-BR.json b/homeassistant/components/tag/translations/pt-BR.json new file mode 100644 index 00000000000..6f21d3e8d6d --- /dev/null +++ b/homeassistant/components/tag/translations/pt-BR.json @@ -0,0 +1,3 @@ +{ + "title": "Etiqueta" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/ru.json b/homeassistant/components/tag/translations/ru.json new file mode 100644 index 00000000000..fdac700612d --- /dev/null +++ b/homeassistant/components/tag/translations/ru.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/zh-Hant.json b/homeassistant/components/tag/translations/zh-Hant.json new file mode 100644 index 00000000000..9b83ec73efc --- /dev/null +++ b/homeassistant/components/tag/translations/zh-Hant.json @@ -0,0 +1,3 @@ +{ + "title": "\u6a19\u7c64" +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/no.json b/homeassistant/components/tibber/translations/no.json index 34e078f5467..4480fb106de 100644 --- a/homeassistant/components/tibber/translations/no.json +++ b/homeassistant/components/tibber/translations/no.json @@ -13,10 +13,8 @@ "data": { "access_token": "Tilgangstoken" }, - "description": "Fyll inn din tilgangstoken fra [https://developer.tibber.com/settings/accesstoken](https://developer.tibber.com/settings/accesstoken)", - "title": "" + "description": "Fyll inn din tilgangstoken fra [https://developer.tibber.com/settings/accesstoken](https://developer.tibber.com/settings/accesstoken)" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/timer/translations/pt-BR.json b/homeassistant/components/timer/translations/pt-BR.json index 0e37123d3ef..ca201a79760 100644 --- a/homeassistant/components/timer/translations/pt-BR.json +++ b/homeassistant/components/timer/translations/pt-BR.json @@ -1,8 +1,8 @@ { "state": { "_": { - "active": "ativo", - "idle": "ocioso", + "active": "Ativo", + "idle": "Ocioso", "paused": "Pausado" } } diff --git a/homeassistant/components/toon/translations/ca.json b/homeassistant/components/toon/translations/ca.json index 3b25d54b974..cec5d4ef851 100644 --- a/homeassistant/components/toon/translations/ca.json +++ b/homeassistant/components/toon/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "L\u2019acord seleccionat ja est\u00e0 configurat.", + "already_configured": "L'acord 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": "Temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3 esgotat.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", @@ -12,7 +12,7 @@ "data": { "agreement": "Acord" }, - "description": "Seleccioneu l'adre\u00e7a de l'acord a afegir.", + "description": "Selecciona l'adre\u00e7a de l'acord a afegir.", "title": "Selecciona acord" }, "pick_implementation": { diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json index c312f98f3d2..a8d6ac5dc23 100644 --- a/homeassistant/components/totalconnect/translations/no.json +++ b/homeassistant/components/totalconnect/translations/no.json @@ -11,8 +11,7 @@ "data": { "password": "Passord", "username": "Brukernavn" - }, - "title": "" + } } } } diff --git a/homeassistant/components/totalconnect/translations/pt-BR.json b/homeassistant/components/totalconnect/translations/pt-BR.json index 30b433108ec..e651bb03b44 100644 --- a/homeassistant/components/totalconnect/translations/pt-BR.json +++ b/homeassistant/components/totalconnect/translations/pt-BR.json @@ -8,6 +8,9 @@ }, "step": { "user": { + "data": { + "username": "Usu\u00e1rio" + }, "title": "Total Connect" } } diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index 48b86516917..d71e2c2c590 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -14,7 +14,6 @@ "host": "Vert", "name": "Navn", "password": "Passord", - "port": "", "username": "Brukernavn" }, "title": "Oppsett av Transmission-klient" diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index 5681f95d984..ca6de86f30b 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -17,8 +17,7 @@ "platform": "Appen der kontoen din registreres", "username": "Brukernavn" }, - "description": "Skriv inn din Tuya-legitimasjon.", - "title": "" + "description": "Skriv inn din Tuya-legitimasjon." } } } diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index dab853f3310..4eedc62396e 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "auth_failed": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", - "conn_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "conn_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { diff --git a/homeassistant/components/twentemilieu/translations/no.json b/homeassistant/components/twentemilieu/translations/no.json index a9d3c184495..0ed6471e4fd 100644 --- a/homeassistant/components/twentemilieu/translations/no.json +++ b/homeassistant/components/twentemilieu/translations/no.json @@ -14,8 +14,7 @@ "house_number": "Husnummer", "post_code": "Postnummer" }, - "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din.", - "title": "" + "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din." } } } diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index a861790ba8d..2742ae6a0c5 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -13,7 +13,6 @@ "data": { "host": "Vert", "password": "Passord", - "port": "", "site": "Nettsted-ID", "username": "Brukernavn", "verify_ssl": "Kontroller bruker riktig sertifikat" diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index eecd5b53c56..550867b682b 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", - "service_unavailable": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "service_unavailable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown_client_mac": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0430 \u044d\u0442\u043e\u043c MAC-\u0430\u0434\u0440\u0435\u0441\u0435." }, "step": { diff --git a/homeassistant/components/upnp/translations/nl.json b/homeassistant/components/upnp/translations/nl.json index 9b579127f8c..cfa845f1b07 100644 --- a/homeassistant/components/upnp/translations/nl.json +++ b/homeassistant/components/upnp/translations/nl.json @@ -10,7 +10,7 @@ "one": "Een", "other": "Ander" }, - "flow_title": "[%%]: {naam}", + "flow_title": "UPnP/IGD: {name}", "step": { "ssdp_confirm": { "description": "Wilt u [%%] instellen?" diff --git a/homeassistant/components/vacuum/translations/pt-BR.json b/homeassistant/components/vacuum/translations/pt-BR.json index 79f4b9b7e42..4e6f8c84748 100644 --- a/homeassistant/components/vacuum/translations/pt-BR.json +++ b/homeassistant/components/vacuum/translations/pt-BR.json @@ -11,5 +11,5 @@ "returning": "Retornando para base" } }, - "title": "Aspirando" + "title": "Aspirador" } \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/ca.json b/homeassistant/components/vizio/translations/ca.json index 50a9751a45d..34ce551a0fe 100644 --- a/homeassistant/components/vizio/translations/ca.json +++ b/homeassistant/components/vizio/translations/ca.json @@ -6,7 +6,8 @@ }, "error": { "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.", + "complete_pairing_failed": "No s'ha pogut completar la vinculaci\u00f3. Verifica que el PIN proporcionat sigui el correcte i que el televisor segueix connectat a la xarxa abans de provar-ho de nou.", + "existing_config_entry_found": "Ja s'ha configurat una entrada de configuraci\u00f3 del Dispositiu VIZIO SmartCast amb el mateix n\u00famero de s\u00e8rie. Per poder configurar aquesta, has de suprimir l'entrada existent.", "host_exists": "Dispositiu VIZIO SmartCast amb aquest nom d'amfitri\u00f3 ja configurat.", "name_exists": "Dispositiu VIZIO SmartCast amb aquest nom ja configurat." }, @@ -15,16 +16,16 @@ "data": { "pin": "PIN" }, - "description": "El televisor hauria d'estar mostrant un codi. Introdueix aquest codi al formulari i segueix amb els seg\u00fcents passos per completar l'emparellament.", - "title": "Proc\u00e9s d'aparellament complet" + "description": "El televisor hauria d'estar mostrant un codi. Introdueix aquest codi al formulari i segueix amb els seg\u00fcents passos per completar la vinculaci\u00f3.", + "title": "Proc\u00e9s de vinculaci\u00f3 complet" }, "pairing_complete": { "description": "El Dispositiu VIZIO SmartCast est\u00e0 connectat a Home Assistant.", - "title": "Emparellament completat" + "title": "Vinculaci\u00f3 completada" }, "pairing_complete_import": { "description": "El Dispositiu VIZIO SmartCast est\u00e0 connectat a Home Assistant.\n\nEl teu [%key::common::config_flow::data::access_token%] \u00e9s '**{access_token}**'.", - "title": "Emparellament completat" + "title": "Vinculaci\u00f3 completada" }, "user": { "data": { @@ -33,7 +34,7 @@ "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "Nom\u00e9s es necessita el [%key::common::config_flow::data::access_token%] per als televisors. Si est\u00e0s configurant un televisor i encara no tens un [%key::common::config_flow::data::access_token%], deixa-ho en blanc per poder fer el proc\u00e9s d'emparellament.", + "description": "Nom\u00e9s es necessita el [%key::common::config_flow::data::access_token%] per als televisors. Si est\u00e0s configurant un televisor i encara no tens un [%key::common::config_flow::data::access_token%], deixa-ho en blanc per poder fer el proc\u00e9s de vinculaci\u00f3.", "title": "Dispositiu VIZIO SmartCast" } } diff --git a/homeassistant/components/vizio/translations/en.json b/homeassistant/components/vizio/translations/en.json index 42dcfe6d96d..0c18c2d581e 100644 --- a/homeassistant/components/vizio/translations/en.json +++ b/homeassistant/components/vizio/translations/en.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Failed to connect", "complete_pairing_failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", + "existing_config_entry_found": "An existing VIZIO SmartCast Device config entry with the same serial number has already been configured. You must delete the existing entry in order to configure this one.", "host_exists": "VIZIO SmartCast Device with specified host already configured.", "name_exists": "VIZIO SmartCast Device with specified name already configured." }, diff --git a/homeassistant/components/vizio/translations/es.json b/homeassistant/components/vizio/translations/es.json index 9daaa0973b7..ec20977b459 100644 --- a/homeassistant/components/vizio/translations/es.json +++ b/homeassistant/components/vizio/translations/es.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "complete_pairing_failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que el televisor sigue encendido y conectado a la red antes de volver a enviarlo.", + "existing_config_entry_found": "Ya se ha configurado una entrada VIZIO SmartCast Device con el mismo n\u00famero de serie. Debes borrar la entrada existente para configurar \u00e9sta.", "host_exists": "Ya existe un VIZIO SmartCast Device configurado con ese host.", "name_exists": "Ya existe un VIZIO SmartCast Device configurado con ese nombre." }, diff --git a/homeassistant/components/vizio/translations/it.json b/homeassistant/components/vizio/translations/it.json index 3877d9458eb..d42562b82b2 100644 --- a/homeassistant/components/vizio/translations/it.json +++ b/homeassistant/components/vizio/translations/it.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "complete_pairing_failed": "Impossibile completare l'associazione. Assicurarsi che il PIN fornito sia corretto e che la TV sia ancora accesa e collegata alla rete prima di inviare nuovamente.", + "existing_config_entry_found": "\u00c8 gi\u00e0 stata configurata una voce di configurazione esistente Dispositivo SmartCast VIZIO con lo stesso numero di serie. \u00c8 necessario eliminare la voce esistente per configurare questa.", "host_exists": "Il Dispositivo SmartCast VIZIO con host specificato \u00e8 gi\u00e0 configurato.", "name_exists": "Il Dispositivo SmartCast VIZIO con il nome specificato \u00e8 gi\u00e0 configurato." }, diff --git a/homeassistant/components/vizio/translations/lb.json b/homeassistant/components/vizio/translations/lb.json index 63160393165..2aa1db3afa6 100644 --- a/homeassistant/components/vizio/translations/lb.json +++ b/homeassistant/components/vizio/translations/lb.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Feeler beim verbannen", "complete_pairing_failed": "Feeler beim ofschl\u00e9isse vun der Kopplung. Iwwerpr\u00e9if dass de PIN korrekt an da de Fernsee nach \u00ebmmer ugeschalt a mam Netzwierk verbonnen ass ier de n\u00e4chste Versuch gestart g\u00ebtt.", + "existing_config_entry_found": "Eng bestoend VIZIO SmartCast Konfiguratioun Entr\u00e9e mat der selwechter Seriennummer ass scho konfigur\u00e9iert. Du musst d\u00e9i existent Entr\u00e9e l\u00e4sche fir d\u00ebs k\u00ebnnen ze konfigur\u00e9ieren.", "host_exists": "VIZIO Apparat mat d\u00ebsem Host ass scho konfigur\u00e9iert.", "name_exists": "VIZIO Apparat mat d\u00ebsen Numm ass scho konfigur\u00e9iert." }, diff --git a/homeassistant/components/vizio/translations/no.json b/homeassistant/components/vizio/translations/no.json index 0f341ce63b5..ede021f1a3a 100644 --- a/homeassistant/components/vizio/translations/no.json +++ b/homeassistant/components/vizio/translations/no.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes.", "complete_pairing_failed": "Kan ikke fullf\u00f8re sammenkoblingen. Forsikre deg om at PIN-koden du oppga er riktig, og at TV-en fortsatt er p\u00e5 og tilkoblet nettverket f\u00f8r du sender inn p\u00e5 nytt.", + "existing_config_entry_found": "En eksisterende VIZIO SmartCast-enhet konfigurasjonsinngang med samme serienummer er allerede konfigurert. Du m\u00e5 slette den eksisterende oppf\u00f8ringen for \u00e5 konfigurere denne.", "host_exists": "VIZIO SmartCast-enhet with specified host already configured.", "name_exists": "VIZIO SmartCast-enhet with specified name already configured." }, diff --git a/homeassistant/components/vizio/translations/pt-BR.json b/homeassistant/components/vizio/translations/pt-BR.json index 425d1b91f5e..bca1aeeaf3d 100644 --- a/homeassistant/components/vizio/translations/pt-BR.json +++ b/homeassistant/components/vizio/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "error": { - "complete_pairing_failed": "N\u00e3o foi poss\u00edvel concluir o pareamento. Verifique se o PIN que voc\u00ea forneceu est\u00e1 correto e a TV ainda est\u00e1 ligada e conectada \u00e0 internet antes de reenviar." + "complete_pairing_failed": "N\u00e3o foi poss\u00edvel concluir o pareamento. Verifique se o PIN que voc\u00ea forneceu est\u00e1 correto e a TV ainda est\u00e1 ligada e conectada \u00e0 internet antes de reenviar.", + "existing_config_entry_found": "Uma entrada j\u00e1 existente configurada com o mesmo n\u00famero de s\u00e9rie j\u00e1 foi configurada. Voc\u00ea deve apagar a entrada existente para poder configurar esta." }, "step": { "pair_tv": { diff --git a/homeassistant/components/vizio/translations/ru.json b/homeassistant/components/vizio/translations/ru.json index d21b0c39615..8211e39959e 100644 --- a/homeassistant/components/vizio/translations/ru.json +++ b/homeassistant/components/vizio/translations/ru.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured_device": "\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.", + "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "updated_entry": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "complete_pairing_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u0412\u0430\u043c\u0438 PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439, \u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a \u0441\u0435\u0442\u0438.", + "existing_config_entry_found": "\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c VIZIO SmartCast \u0441 \u0442\u0435\u043c \u0436\u0435 \u0441\u0430\u043c\u044b\u043c \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c, \u0447\u0442\u043e\u0431\u044b \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0451.", "host_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "name_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index 0ddf8f8b88f..add487060ad 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "complete_pairing_failed": "\u7121\u6cd5\u5b8c\u6210\u914d\u5c0d\uff0c\u50b3\u9001\u524d\u3001\u8acb\u78ba\u5b9a\u6240\u8f38\u5165\u7684 PIN \u78bc\u3001\u540c\u6642\u96fb\u8996\u5df2\u7d93\u958b\u555f\u4e26\u9023\u7dda\u81f3\u7db2\u8def\u3002", + "existing_config_entry_found": "\u5df2\u6709\u4e00\u7d44\u4f7f\u7528\u76f8\u540c\u5e8f\u865f\u7684 VIZIO SmartCast \u8a2d\u5099 \u5df2\u8a2d\u5b9a\u3002\u5fc5\u9808\u5148\u9032\u884c\u522a\u9664\u5f8c\u624d\u80fd\u91cd\u65b0\u8a2d\u5b9a\u3002", "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b VIZIO SmartCast \u8a2d\u5099 \u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", "name_exists": "\u4f9d\u540d\u7a31\u4e4b VIZIO SmartCast \u8a2d\u5099 \u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002" }, diff --git a/homeassistant/components/volumio/translations/ru.json b/homeassistant/components/volumio/translations/ru.json index 84267ff4a4e..905ecbe8e4b 100644 --- a/homeassistant/components/volumio/translations/ru.json +++ b/homeassistant/components/volumio/translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u043c\u0443 Volumio." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/wilight/translations/ca.json b/homeassistant/components/wilight/translations/ca.json new file mode 100644 index 00000000000..5920e54d258 --- /dev/null +++ b/homeassistant/components/wilight/translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "not_supported_device": "Actualment aquest WiLight no \u00e9s compatible", + "not_wilight_device": "Aquest dispositiu no \u00e9s WiLight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "Voleu configurar el WiLight {name}? \n\n Admet: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/en.json b/homeassistant/components/wilight/translations/en.json index fb121e3b2fa..14724af4ae7 100644 --- a/homeassistant/components/wilight/translations/en.json +++ b/homeassistant/components/wilight/translations/en.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured_device": "This WiLight is already configured", - "not_supported_device": "This WiLight is currently not supported", - "not_wilight_device": "This Device is not WiLight" + "already_configured": "Device is already configured", + "not_supported_device": "This WiLight is currently not supported", + "not_wilight_device": "This Device is not WiLight" }, - "flow_title": "WiLight: {name}", - "step": { - "confirm": { - "title": "WiLight", - "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}" - } - } + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}", + "title": "WiLight" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/es.json b/homeassistant/components/wilight/translations/es.json new file mode 100644 index 00000000000..b0104e6e44c --- /dev/null +++ b/homeassistant/components/wilight/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "not_supported_device": "Este WiLight no es compatible actualmente", + "not_wilight_device": "Este dispositivo no es un Wilight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "\u00bfQuieres configurar WiLight {name} ? \n\n Es compatible con: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/fr.json b/homeassistant/components/wilight/translations/fr.json new file mode 100644 index 00000000000..2368c95fd88 --- /dev/null +++ b/homeassistant/components/wilight/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "not_supported_device": "Ce WiLight n'est actuellement pas pris en charge", + "not_wilight_device": "Cet appareil n'est pas WiLight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/it.json b/homeassistant/components/wilight/translations/it.json new file mode 100644 index 00000000000..eb9cdcc8a55 --- /dev/null +++ b/homeassistant/components/wilight/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "not_supported_device": "Questo WiLight non \u00e8 attualmente supportato", + "not_wilight_device": "Questo dispositivo non \u00e8 WiLight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "Vuoi configurare WiLight {name}? \n\nSupporta: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/lb.json b/homeassistant/components/wilight/translations/lb.json new file mode 100644 index 00000000000..2c0b831098e --- /dev/null +++ b/homeassistant/components/wilight/translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "not_supported_device": "D\u00ebse WiLight g\u00ebtt aktuell net \u00ebnnerst\u00ebtzt", + "not_wilight_device": "D\u00ebsen Apparat ass kee WiLight" + }, + "flow_title": "Wilight: {name}", + "step": { + "confirm": { + "description": "Soll WiLight {name} konfigur\u00e9iert ginn?\n\nEt \u00ebnnerst\u00ebtzt: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/no.json b/homeassistant/components/wilight/translations/no.json new file mode 100644 index 00000000000..d57458d49ea --- /dev/null +++ b/homeassistant/components/wilight/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "not_supported_device": "Dette WiLight st\u00f8ttes forel\u00f8pig ikke", + "not_wilight_device": "Denne enheten er ikke WiLight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "Vil du konfigurere WiLight {name} ? \n\n Den st\u00f8tter: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/ru.json b/homeassistant/components/wilight/translations/ru.json new file mode 100644 index 00000000000..9842b810f38 --- /dev/null +++ b/homeassistant/components/wilight/translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "not_supported_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "not_wilight_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 WiLight." + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c WiLight {name}? \n\n\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/zh-Hant.json b/homeassistant/components/wilight/translations/zh-Hant.json new file mode 100644 index 00000000000..8859e831d57 --- /dev/null +++ b/homeassistant/components/wilight/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_supported_device": "\u4e0d\u652f\u63f4\u6b64\u6b3e WiLight \u8a2d\u5099\u3002", + "not_wilight_device": "\u6b64\u8a2d\u5099\u4e26\u975e WiLight" + }, + "flow_title": "WiLight\uff1a{name}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a WiLight {name}\uff1f\n\n\u652f\u63f4\uff1a{components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/lb.json b/homeassistant/components/wolflink/translations/lb.json index 97a65b12d02..05ce6a27797 100644 --- a/homeassistant/components/wolflink/translations/lb.json +++ b/homeassistant/components/wolflink/translations/lb.json @@ -19,7 +19,8 @@ "data": { "password": "Passwuert", "username": "Benotzernumm" - } + }, + "title": "WOLF SmartSet Verbindung" } } } diff --git a/homeassistant/components/wolflink/translations/pt-BR.json b/homeassistant/components/wolflink/translations/pt-BR.json new file mode 100644 index 00000000000..43e2720b365 --- /dev/null +++ b/homeassistant/components/wolflink/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na conex\u00e3o" + }, + "step": { + "device": { + "data": { + "device_name": "Dispositivo" + } + }, + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/ru.json b/homeassistant/components/wolflink/translations/ru.json index a15fb94c8ff..841f7b26030 100644 --- a/homeassistant/components/wolflink/translations/ru.json +++ b/homeassistant/components/wolflink/translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\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." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/wolflink/translations/sensor.ko.json b/homeassistant/components/wolflink/translations/sensor.ko.json index 99e965e1b21..5b6c33d7231 100644 --- a/homeassistant/components/wolflink/translations/sensor.ko.json +++ b/homeassistant/components/wolflink/translations/sensor.ko.json @@ -6,7 +6,15 @@ "absenkbetrieb": "\uc911\ub2e8 \ub300\uccb4 \ubaa8\ub4dc", "absenkstop": "\uc911\ub2e8 \ub300\uccb4 \uc911\uc9c0", "aktiviert": "\ud65c\uc131\ud654", - "antilegionellenfunktion": "\ud56d \ub808\uc9c0\uc624\ub12c\ub77c\uade0 \uae30\ub2a5" + "antilegionellenfunktion": "\ud56d \ub808\uc9c0\uc624\ub12c\ub77c\uade0 \uae30\ub2a5", + "at_abschaltung": "OT \ub044\uae30", + "at_frostschutz": "OT \ube59\uacb0 \ubcf4\ud638", + "aus": "\ube44\ud65c\uc131\ud654", + "auto": "\uc790\ub3d9", + "auto_off_cool": "\ub0c9\ubc29 \uc790\ub3d9 \uaebc\uc9d0", + "auto_on_cool": "\ub0c9\ubc29 \uc790\ub3d9 \ucf1c\uc9d0", + "automatik_aus": "\uc790\ub3d9 \uaebc\uc9d0", + "automatik_ein": "\uc790\ub3d9 \ucf1c\uc9d0" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.lb.json b/homeassistant/components/wolflink/translations/sensor.lb.json index 94c46e4f362..b57d2c8747a 100644 --- a/homeassistant/components/wolflink/translations/sensor.lb.json +++ b/homeassistant/components/wolflink/translations/sensor.lb.json @@ -2,7 +2,11 @@ "state": { "wolflink__state": { "1_x_warmwasser": "1x DHW", + "abgasklappe": "Ofgas Klapp", + "absenkbetrieb": "Absenk Betrib", + "absenkstop": "Absenk Stop", "aktiviert": "Aktiv\u00e9iert", + "antilegionellenfunktion": "Anti-legionelle Funktioun", "at_abschaltung": "OT ausmaachen", "at_frostschutz": "OT Frostschutz", "aus": "Deaktiv\u00e9iert", @@ -12,18 +16,31 @@ "automatik_aus": "Automatik AUS", "automatik_ein": "Automatik UN", "bereit_keine_ladung": "Prett, lued net", + "betrieb_ohne_brenner": "Betrib ouni Brenner", "cooling": "Ofkillen", "deaktiviert": "Inaktiv", "eco": "Eco", "ein": "Aktiv\u00e9iert", + "estrichtrocknung": "Chape dr\u00e9chnen", + "externe_deaktivierung": "Externe D\u00e9aktivatioun", + "fernschalter_ein": "Fernsteierung aktiv\u00e9iert", "frostschutz": "Frostschutz", "gasdruck": "Gas Drock", "glt_betrieb": "BMS Modus", "heizbetrieb": "Heizung Modus", + "heizgerat_mit_speicher": "Boiler mat Sp\u00e4icher", "heizung": "Heizung", "initialisierung": "Initialis\u00e9ierung", "kalibration": "Kalibratioun", + "kalibration_heizbetrieb": "Heizung Betrib Kalibratioun", + "kalibration_kombibetrieb": "Kombi Betrib Kalibratioun", "kalibration_warmwasserbetrieb": "DHW Kalibratioun", + "kombibetrieb": "Kombi Betriib", + "kombigerat": "Kombiger\u00e4t", + "kombigerat_mit_solareinbindung": "Kombi Boiler mat Solar Integratioun", + "mindest_kombizeit": "Mindest Kombi Z\u00e4it", + "nachspulen": "Nospullen", + "nur_heizgerat": "N\u00ebmme Boiler", "parallelbetrieb": "Parrallel Modus", "partymodus": "Party Modus", "permanent": "Permanent", @@ -31,7 +48,9 @@ "reduzierter_betrieb": "Limit\u00e9ierte Modus", "rt_abschaltung": "RT ausmaachen", "rt_frostschutz": "RT Frostschutz", + "ruhekontakt": "Rou Kontakt", "schornsteinfeger": "Emissioun Test", + "smart_grid": "SmartGrid", "smart_home": "SmartHome", "softstart": "Soft Start", "solarbetrieb": "Solar Modus", @@ -43,12 +62,17 @@ "standby": "Standby", "start": "Start", "storung": "Feeler", + "taktsperre": "Takt Sp\u00e4r", + "telefonfernschalter": "Telefon Fernsteierung", "test": "Test", "tpw": "TPW", "urlaubsmodus": "Vakanze Modus", + "ventilprufung": "Ventil Test", + "vorspulen": "Virspullen", "warmwasser": "DHW", "warmwasser_schnellstart": "DHW Schnell Start", "warmwasserbetrieb": "DHW Modus", + "warmwassernachlauf": "Waarmt Waaser Nolaf", "warmwasservorrang": "DHW Priorit\u00e9it", "zunden": "Z\u00fcndung" } diff --git a/homeassistant/components/wolflink/translations/sensor.pt-BR.json b/homeassistant/components/wolflink/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..2af363e8f1c --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.pt-BR.json @@ -0,0 +1,18 @@ +{ + "state": { + "wolflink__state": { + "aktiviert": "Ativado", + "aus": "Desativado", + "deaktiviert": "Inativo", + "eco": "Econ\u00f4mico", + "ein": "Habilitado", + "stabilisierung": "Estabiliza\u00e7\u00e3o", + "standby": "Em espera", + "start": "Iniciar", + "storung": "Falha", + "test": "Teste", + "urlaubsmodus": "Modo de f\u00e9rias", + "ventilprufung": "Teste de v\u00e1lvula" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ca.json b/homeassistant/components/xiaomi_aqara/translations/ca.json index 3b90529d740..534a6b3654e 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ca.json +++ b/homeassistant/components/xiaomi_aqara/translations/ca.json @@ -7,8 +7,10 @@ }, "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_host": "Adre\u00e7a IP no v\u00e0lida", "invalid_interface": "Interf\u00edcie de xarxa no v\u00e0lida", "invalid_key": "Clau de la passarel\u00b7la no v\u00e0lida", + "invalid_mac": "Adre\u00e7a MAC 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}", @@ -30,9 +32,11 @@ }, "user": { "data": { - "interface": "Interf\u00edcie de xarxa a utilitzar" + "host": "Adre\u00e7a IP (opcional)", + "interface": "Interf\u00edcie de xarxa a utilitzar", + "mac": "Adre\u00e7a MAC (opcional)" }, - "description": "Connecta't a la passarel\u00b7la Xiaomi Aqara", + "description": "Connecta't a la teva passarel\u00b7la Xiaomi Aqara, si les adreces IP i MAC es deixen buides s'utilitzar\u00e0 els descobriment autom\u00e0tic", "title": "Passarel\u00b7la Xiaomi Aqara" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json index b9f6fa7ab2a..25d44d80b54 100644 --- a/homeassistant/components/xiaomi_aqara/translations/en.json +++ b/homeassistant/components/xiaomi_aqara/translations/en.json @@ -1,16 +1,17 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured": "Device is already configured", "already_in_progress": "Config flow for this gateway is already in progress", "not_xiaomi_aqara": "Not a Xiaomi Aqara Gateway, discovered device did not match known gateways" }, "error": { "discovery_error": "Failed to discover a Xiaomi Aqara Gateway, try using the IP of the device running HomeAssistant as interface", - "invalid_host": "Invalid [%key:common::config_flow::data::ip%]", + "invalid_host": "Invalid IP Address", "invalid_interface": "Invalid network interface", "invalid_key": "Invalid gateway key", - "invalid_mac": "Invalid Mac Address" + "invalid_mac": "Invalid Mac Address", + "not_found_error": "Zeroconf discovered Gateway could not be located to get the necessary information, try using the IP of the device running HomeAssistant as interface" }, "flow_title": "Xiaomi Aqara Gateway: {name}", "step": { @@ -31,7 +32,7 @@ }, "user": { "data": { - "host": "[%key:common::config_flow::data::ip%] (optional)", + "host": "IP Address (optional)", "interface": "The network interface to use", "mac": "Mac Address (optional)" }, diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index 9d388203bcb..caf99aed710 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -7,8 +7,10 @@ }, "error": { "discovery_error": "No se pudo descubrir un Xiaomi Aqara Gateway, intenta utilizar la IP del dispositivo que ejecuta HomeAssistant como interfaz", + "invalid_host": "Direcci\u00f3n IP no v\u00e1lida", "invalid_interface": "Interfaz de red inv\u00e1lida", "invalid_key": "Clave del gateway inv\u00e1lida", + "invalid_mac": "Direcci\u00f3n Mac no v\u00e1lida", "not_found_error": "El Gateway descubierto por Zeroconf no puede localizarse para obtener toda la informaci\u00f3n necesaria, intenta usar la IP del dispositivo que ejecuta HomeAssistant como interfaz" }, "flow_title": "Xiaomi Aqara Gateway: {name}", @@ -30,7 +32,9 @@ }, "user": { "data": { - "interface": "La interfaz de la red a usar" + "host": "Direcci\u00f3n IP (opcional)", + "interface": "La interfaz de la red a usar", + "mac": "Direcci\u00f3n Mac (opcional)" }, "description": "Conectar con tu Xiaomi Aqara Gateway", "title": "Xiaomi Aqara Gateway" diff --git a/homeassistant/components/xiaomi_aqara/translations/fy.json b/homeassistant/components/xiaomi_aqara/translations/fy.json new file mode 100644 index 00000000000..19bc44d2734 --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/fy.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mac": "MAC-adres (opsjoneel)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index bc64862afec..6b7f6d907ae 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -7,8 +7,10 @@ }, "error": { "discovery_error": "Impossibile individuare un gateway Xiaomi Aqara, provare a utilizzare l'IP del dispositivo che esegue HomeAssistant come interfaccia", + "invalid_host": "Indirizzo IP non valido", "invalid_interface": "Interfaccia di rete non valida", "invalid_key": "Chiave gateway non valida", + "invalid_mac": "Indirizzo Mac non valido", "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}", @@ -30,9 +32,11 @@ }, "user": { "data": { - "interface": "L'interfaccia di rete da utilizzare" + "host": "Indirizzo IP (opzionale)", + "interface": "L'interfaccia di rete da utilizzare", + "mac": "Indirizzo Mac (opzionale)" }, - "description": "Connettiti al tuo Xiaomi Aqara Gateway", + "description": "Connettiti al tuo Xiaomi Aqara Gateway, se gli indirizzi IP e mac sono lasciati vuoti, verr\u00e0 utilizzato il rilevamento automatico", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/ko.json b/homeassistant/components/xiaomi_aqara/translations/ko.json index 38f58148e4c..90a22ace2b8 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ko.json +++ b/homeassistant/components/xiaomi_aqara/translations/ko.json @@ -32,7 +32,7 @@ "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", + "description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \ubc0f Mac \uc8fc\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4", "title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/lb.json b/homeassistant/components/xiaomi_aqara/translations/lb.json index bfea843c56e..919fdeb21ea 100644 --- a/homeassistant/components/xiaomi_aqara/translations/lb.json +++ b/homeassistant/components/xiaomi_aqara/translations/lb.json @@ -7,8 +7,10 @@ }, "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_host": "Ong\u00eblteg IP Adress", "invalid_interface": "Ong\u00ebltege Netzwierk Interface", "invalid_key": "Ong\u00ebltegen Gateway Schl\u00ebssel", + "invalid_mac": "Ong\u00eblteg Mac Adresse", "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}", @@ -30,7 +32,9 @@ }, "user": { "data": { - "interface": "Netzwierk Interface dee soll benotzt ginn" + "host": "IP Adress (Optionell)", + "interface": "Netzwierk Interface dee soll benotzt ginn", + "mac": "Mac Adress (Optionell)" }, "description": "Mat denger Xiaomi Aqara Gateway verbannen", "title": "Xiaomi Aqara Gateway" diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json new file mode 100644 index 00000000000..24eeccf11a0 --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mac": "MAC-adres (optioneel)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json index c94d79b16d5..c3dc7f9ac5d 100644 --- a/homeassistant/components/xiaomi_aqara/translations/no.json +++ b/homeassistant/components/xiaomi_aqara/translations/no.json @@ -7,16 +7,14 @@ }, "error": { "discovery_error": "Kunne ikke oppdage en Xiaomi Aqara Gateway, pr\u00f8v \u00e5 bruke IP-adressen til enheten som kj\u00f8rer HomeAssistant som grensesnitt", + "invalid_host": "Ugyldig IP adresse", "invalid_interface": "Ugyldig nettverksgrensesnitt", "invalid_key": "Ugyldig gateway-n\u00f8kkel", + "invalid_mac": "Ugyldig Mac-adresse", "not_found_error": "Zeroconf oppdaget Gateway kunne ikke v\u00e6re plassert for \u00e5 f\u00e5 den n\u00f8dvendige informasjonen, kan du pr\u00f8ve \u00e5 bruke IP-adressen til enheten som kj\u00f8rer HomeAssistant som grensesnitt" }, - "flow_title": "", "step": { "select": { - "data": { - "select_ip": "" - }, "description": "Kj\u00f8r oppsettet igjen hvis du vil koble til tilleggsportaler", "title": "Velg Xiaomi Aqara Gateway som du \u00f8nsker \u00e5 koble til" }, @@ -30,10 +28,11 @@ }, "user": { "data": { - "interface": "Nettverksgrensesnittet som skal brukes" + "host": "IP adresse (valgfritt)", + "interface": "Nettverksgrensesnittet som skal brukes", + "mac": "Mac-adresse (valgfritt)" }, - "description": "Koble til Xiaomi Aqara Gateway", - "title": "" + "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og mac-adressene er tomme, brukes automatisk oppdagelse" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/pl.json b/homeassistant/components/xiaomi_aqara/translations/pl.json index d7d2e4fec3e..46a18c457af 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pl.json +++ b/homeassistant/components/xiaomi_aqara/translations/pl.json @@ -7,8 +7,10 @@ }, "error": { "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki Xiaomi Aqara, spr\u00f3buj u\u017cy\u0107 adresu IP urz\u0105dzenia, na kt\u00f3rym pracuje Home Assistant jako interfejsu.", + "invalid_host": "Adres IP jest nieprawid\u0142owy.", "invalid_interface": "Nieprawid\u0142owy interfejs sieciowy.", "invalid_key": "Nieprawid\u0142owy klucz bramki.", + "invalid_mac": "Nieprawid\u0142owy adres MAC.", "not_found_error": "Nie mo\u017cna odnale\u017a\u0107 wykrytej bramki, aby uzyska\u0107 niezb\u0119dne informacje, spr\u00f3buj u\u017cy\u0107 adresu IP urz\u0105dzenia, na kt\u00f3rym pracuje Home Assistant jako interfejsu." }, "flow_title": "Bramka Xiaomi Aqara: {name}", @@ -30,7 +32,9 @@ }, "user": { "data": { - "interface": "Interfejs sieciowy" + "host": "Adres IP (opcjonalnie)", + "interface": "Interfejs sieciowy", + "mac": "Adres MAC (opcjonalnie)" }, "description": "Po\u0142\u0105cz si\u0119 z bramk\u0105 Xiaomi Aqara", "title": "Bramka Xiaomi Aqara" diff --git a/homeassistant/components/xiaomi_aqara/translations/pt.json b/homeassistant/components/xiaomi_aqara/translations/pt.json index 99dcae6ca47..98d4f8ff63f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt.json @@ -1,11 +1,6 @@ { "config": { "step": { - "select": { - "data": { - "select_ip": "" - } - }, "settings": { "data": { "name": "Nome da Gateway" diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 70c19bf2aec..79bea5f6e44 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -1,14 +1,16 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f.", "not_xiaomi_aqara": "\u042d\u0442\u043e \u043d\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara. \u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u043c \u0448\u043b\u044e\u0437\u0430\u043c." }, "error": { "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437 Xiaomi Aqara, \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.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.", "invalid_interface": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0430.", + "invalid_mac": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 MAC-\u0430\u0434\u0440\u0435\u0441.", "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": "\u0428\u043b\u044e\u0437 Xiaomi Aqara: {name}", @@ -30,9 +32,11 @@ }, "user": { "data": { - "interface": "\u0421\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441" + "host": "IP-\u0430\u0434\u0440\u0435\u0441 (optional)", + "interface": "\u0421\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441", + "mac": "MAC-\u0430\u0434\u0440\u0435\u0441 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "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.", + "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. \u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0438 MAC \u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u043f\u0443\u0441\u0442\u044b\u043c\u0438.", "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/tr.json b/homeassistant/components/xiaomi_aqara/translations/tr.json new file mode 100644 index 00000000000..10d1374187e --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_mac": "Ge\u00e7ersiz Mac Adresi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 99b677ddd7d..955c5d39108 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -7,8 +7,10 @@ }, "error": { "discovery_error": "\u63a2\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u8a2d\u5099\u7684 IP \u4f5c\u70ba\u4ecb\u9762", + "invalid_host": "\u7121\u6548\u7684 IP \u4f4d\u5740", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548", "invalid_key": "\u7db2\u95dc\u5bc6\u9470\u7121\u6548", + "invalid_mac": "\u7121\u6548\u7684 Mac \u4f4d\u5740", "not_found_error": "Zeroconf \u6240\u63a2\u7d22\u7684\u7db2\u95dc\u7121\u6cd5\u53d6\u5f97\u5fc5\u8981\u7684\u8cc7\u8a0a\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u7684\u8a2d\u5099 IP \u4f5c\u70ba\u4ecb\u9762" }, "flow_title": "\u5c0f\u7c73 Aqara \u7db2\u95dc\uff1a{name}", @@ -30,9 +32,11 @@ }, "user": { "data": { - "interface": "\u4f7f\u7528\u7684\u7db2\u8def\u4ecb\u9762" + "host": "IP \u4f4d\u5740\uff08\u9078\u9805\uff09", + "interface": "\u4f7f\u7528\u7684\u7db2\u8def\u4ecb\u9762", + "mac": "Mac \u4f4d\u5740\uff08\u9078\u9805\uff09" }, - "description": "\u9023\u7dda\u81f3\u5c0f\u7c73 Aqara \u7db2\u95dc", + "description": "\u9023\u7dda\u81f3\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u5047\u5982 IP \u6216 Mac \u4f4d\u5740\u70ba\u7a7a\u767d\u3001\u5c07\u9032\u884c\u81ea\u52d5\u63a2\u7d22", "title": "\u5c0f\u7c73 Aqara \u7db2\u95dc" } } diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index cf978d4a015..ff0ba48d98a 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -23,8 +23,7 @@ "data": { "gateway": "Koble til en Xiaomi Gateway" }, - "description": "Velg hvilken enhet du vil koble til.", - "title": "" + "description": "Velg hvilken enhet du vil koble til." } } } diff --git a/homeassistant/components/xiaomi_miio/translations/pt-BR.json b/homeassistant/components/xiaomi_miio/translations/pt-BR.json index 2f7f84af27e..4c5e0bf1ccb 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_miio/translations/pt-BR.json @@ -2,6 +2,16 @@ "config": { "abort": { "already_in_progress": "O fluxo de configura\u00e7\u00e3o para este dispositivo Xiaomi Miio j\u00e1 est\u00e1 em andamento." + }, + "error": { + "connect_error": "Falha na conex\u00e3o" + }, + "step": { + "gateway": { + "data": { + "host": "Endere\u00e7o IP" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index d8c1d84e2b6..1c934e2784c 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\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.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f." }, "error": { - "connect_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "connect_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432." }, "flow_title": "Xiaomi Miio: {name}", diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 8b2e9a9aed0..c1092875b01 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -65,6 +65,7 @@ "device_dropped": "Dispositiu caigut", "device_flipped": "Dispositiu voltejat a \"{subtype}\"", "device_knocked": "Dispositiu colpejat a \"{subtype}\"", + "device_offline": "Dispositiu desconnectat", "device_rotated": "Dispositiu rotat a \"{subtype}\"", "device_shaken": "Dispositiu sacsejat", "device_slid": "Dispositiu lliscat a \"{subtype}\"", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 6a1eb4bac8e..d54eec90b5f 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -65,6 +65,7 @@ "device_dropped": "Device dropped", "device_flipped": "Device flipped \"{subtype}\"", "device_knocked": "Device knocked \"{subtype}\"", + "device_offline": "Device offline", "device_rotated": "Device rotated \"{subtype}\"", "device_shaken": "Device shaken", "device_slid": "Device slid \"{subtype}\"", diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index fd06722a445..fb2445f0f8c 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -65,6 +65,7 @@ "device_dropped": "Dispositivo ca\u00eddo", "device_flipped": "Dispositivo volteado \" {subtype} \"", "device_knocked": "Dispositivo eliminado \" {subtype} \"", + "device_offline": "Dispositivo desconectado", "device_rotated": "Dispositivo girado \" {subtype} \"", "device_shaken": "Dispositivo agitado", "device_slid": "Dispositivo deslizado \" {subtype} \"", diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 7b2531c1290..8330583cf74 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -65,6 +65,7 @@ "device_dropped": "Dispositivo caduto", "device_flipped": "Dispositivo capovolto \" {subtype} \"", "device_knocked": "Dispositivo bussato \" {subtype} \"", + "device_offline": "Dispositivo offline", "device_rotated": "Dispositivo ruotato \" {subtype} \"", "device_shaken": "Dispositivo in vibrazione", "device_slid": "Dispositivo scivolato \"{sottotipo}\"", diff --git a/homeassistant/components/zha/translations/lb.json b/homeassistant/components/zha/translations/lb.json index 62498a7dfe2..e2c4a63f8d7 100644 --- a/homeassistant/components/zha/translations/lb.json +++ b/homeassistant/components/zha/translations/lb.json @@ -65,6 +65,7 @@ "device_dropped": "Apparat gefall", "device_flipped": "Apparat \u00ebmgedr\u00e9int \"{subtype}\"", "device_knocked": "Apparat geklappt \"{subtype}\"", + "device_offline": "Apparat net ereechbar", "device_rotated": "Apparat gedr\u00e9int \"{subtype}\"", "device_shaken": "Apparat ger\u00ebselt", "device_slid": "Apparat gerutscht \"{subtype}\"", diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 9699a7219ad..c5112235239 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -27,8 +27,7 @@ "data": { "path": "Seriell enhetsbane" }, - "description": "Velg seriell port for Zigbee radio", - "title": "" + "description": "Velg seriell port for Zigbee radio" } } }, @@ -65,6 +64,7 @@ "device_dropped": "Enhet droppet", "device_flipped": "Enheten snudd \"{subtype}\"", "device_knocked": "Enheten sl\u00e5tt \"{subtype}\"", + "device_offline": "Enheten er frakoblet", "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", "device_slid": "Enheten skled \"{subtype}\"", diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 4b943032d28..e06bff43993 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -29,6 +29,12 @@ "action_type": { "squawk": "Squawk", "warn": "Aviso" + }, + "trigger_subtype": { + "close": "Fechado" + }, + "trigger_type": { + "device_offline": "Dispositivo offline" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index d535e9dd588..eea55972c62 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -65,6 +65,7 @@ "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u043b\u0438", "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 {subtype}", "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 {subtype}", + "device_offline": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0432 \u0441\u0435\u0442\u0438", "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 {subtype}", "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438", "device_slid": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438 {subtype}", diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index cbe4d8fbedd..5965911d459 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -65,6 +65,7 @@ "device_dropped": "\u8a2d\u5099\u6389\u843d", "device_flipped": "\u7ffb\u52d5 \"{subtype}\" \u8a2d\u5099", "device_knocked": "\u6572\u64ca \"{subtype}\" \u8a2d\u5099", + "device_offline": "\u8a2d\u5099\u96e2\u7dda", "device_rotated": "\u65cb\u8f49 \"{subtype}\" \u8a2d\u5099", "device_shaken": "\u8a2d\u5099\u6416\u6643", "device_slid": "\u63a8\u52d5 \"{subtype}\" \u8a2d\u5099", diff --git a/homeassistant/components/zone/translations/no.json b/homeassistant/components/zone/translations/no.json index 9bf6e189369..415c0a6afaa 100644 --- a/homeassistant/components/zone/translations/no.json +++ b/homeassistant/components/zone/translations/no.json @@ -10,8 +10,7 @@ "latitude": "Breddegrad", "longitude": "Lengdegrad", "name": "Navn", - "passive": "Passiv", - "radius": "" + "passive": "Passiv" }, "title": "Definer sone parametere" } From e4d29bf3ec39cddd54a7f30a6c16879f73a2cf2c Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 27 Aug 2020 00:43:17 -0300 Subject: [PATCH 367/862] Add tests for Broadlink sensors (#39230) * Add tests for Broadlink sensors * Remove sensor.py from .coveragerc --- .coveragerc | 1 - tests/components/broadlink/__init__.py | 30 +++ tests/components/broadlink/test_sensors.py | 287 +++++++++++++++++++++ 3 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 tests/components/broadlink/test_sensors.py diff --git a/.coveragerc b/.coveragerc index 6566b4caad6..72f5ffa5812 100644 --- a/.coveragerc +++ b/.coveragerc @@ -103,7 +103,6 @@ omit = homeassistant/components/broadlink/__init__.py homeassistant/components/broadlink/const.py homeassistant/components/broadlink/remote.py - homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/switch.py homeassistant/components/broadlink/updater.py homeassistant/components/brottsplatskartan/sensor.py diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 622fe4fba40..5d6c3312e51 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -6,6 +6,16 @@ from tests.common import MockConfigEntry # Do not edit/remove. Adding is ok. BROADLINK_DEVICES = { + "Entrance": ( + "192.168.0.11", + "34ea34befc25", + "RM mini 3", + "Broadlink", + "RM2", + 0x2737, + 57, + 8, + ), "Living Room": ( "192.168.0.12", "34ea34b43b5a", @@ -26,6 +36,26 @@ BROADLINK_DEVICES = { 20025, 7, ), + "Garage": ( + "192.168.0.14", + "34ea34c43f31", + "RM4 pro", + "Broadlink", + "RM4", + 0x6026, + 52, + 4, + ), + "Bedroom": ( + "192.168.0.15", + "34ea34b45d2c", + "e-Sensor", + "Broadlink", + "A1", + 0x2714, + 20025, + 5, + ), } diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py new file mode 100644 index 00000000000..8d1f7be9e5d --- /dev/null +++ b/tests/components/broadlink/test_sensors.py @@ -0,0 +1,287 @@ +"""Tests for Broadlink sensors.""" +from homeassistant.components.broadlink.const import DOMAIN, SENSOR_DOMAIN +from homeassistant.helpers.entity_registry import async_entries_for_device + +from . import get_device + +from tests.async_mock import patch +from tests.common import mock_device_registry, mock_registry + + +async def test_a1_sensor_setup(hass): + """Test a successful e-Sensor setup.""" + device = get_device("Bedroom") + mock_api = device.get_mock_api() + mock_api.check_sensors_raw.return_value = { + "temperature": 27.4, + "humidity": 59.3, + "air_quality": 3, + "light": 2, + "noise": 1, + } + mock_entry = device.get_mock_entry() + mock_entry.add_to_hass(hass) + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + with patch("broadlink.gendevice", return_value=mock_api): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_api.check_sensors_raw.call_count == 1 + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 5 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == { + (f"{device.name} Temperature", "27.4"), + (f"{device.name} Humidity", "59.3"), + (f"{device.name} Air Quality", "3"), + (f"{device.name} Light", "2"), + (f"{device.name} Noise", "1"), + } + + +async def test_a1_sensor_update(hass): + """Test a successful e-Sensor update.""" + device = get_device("Bedroom") + mock_api = device.get_mock_api() + mock_api.check_sensors_raw.return_value = { + "temperature": 22.4, + "humidity": 47.3, + "air_quality": 3, + "light": 2, + "noise": 1, + } + mock_entry = device.get_mock_entry() + mock_entry.add_to_hass(hass) + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + with patch("broadlink.gendevice", return_value=mock_api): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 5 + + mock_api.check_sensors_raw.return_value = { + "temperature": 22.5, + "humidity": 47.4, + "air_quality": 2, + "light": 3, + "noise": 2, + } + await hass.helpers.entity_component.async_update_entity( + next(iter(sensors)).entity_id + ) + assert mock_api.check_sensors_raw.call_count == 2 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == { + (f"{device.name} Temperature", "22.5"), + (f"{device.name} Humidity", "47.4"), + (f"{device.name} Air Quality", "2"), + (f"{device.name} Light", "3"), + (f"{device.name} Noise", "2"), + } + + +async def test_rm_pro_sensor_setup(hass): + """Test a successful RM pro sensor setup.""" + device = get_device("Office") + mock_api = device.get_mock_api() + mock_api.check_sensors.return_value = {"temperature": 18.2} + mock_entry = device.get_mock_entry() + mock_entry.add_to_hass(hass) + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + with patch("broadlink.gendevice", return_value=mock_api): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_api.check_sensors.call_count == 1 + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 1 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == {(f"{device.name} Temperature", "18.2")} + + +async def test_rm_pro_sensor_update(hass): + """Test a successful RM pro sensor update.""" + device = get_device("Office") + mock_api = device.get_mock_api() + mock_api.check_sensors.return_value = {"temperature": 25.7} + mock_entry = device.get_mock_entry() + mock_entry.add_to_hass(hass) + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + with patch("broadlink.gendevice", return_value=mock_api): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 1 + + mock_api.check_sensors.return_value = {"temperature": 25.8} + await hass.helpers.entity_component.async_update_entity( + next(iter(sensors)).entity_id + ) + assert mock_api.check_sensors.call_count == 2 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == {(f"{device.name} Temperature", "25.8")} + + +async def test_rm_mini3_no_sensor(hass): + """Test we do not set up sensors for RM mini 3.""" + device = get_device("Entrance") + mock_api = device.get_mock_api() + mock_api.check_sensors.return_value = {"temperature": 0} + mock_entry = device.get_mock_entry() + mock_entry.add_to_hass(hass) + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + with patch("broadlink.gendevice", return_value=mock_api): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_api.check_sensors.call_count <= 1 + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 0 + + +async def test_rm4_pro_hts2_sensor_setup(hass): + """Test a successful RM4 pro sensor setup with HTS2 cable.""" + device = get_device("Garage") + mock_api = device.get_mock_api() + mock_api.check_sensors.return_value = {"temperature": 22.5, "humidity": 43.7} + mock_entry = device.get_mock_entry() + mock_entry.add_to_hass(hass) + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + with patch("broadlink.gendevice", return_value=mock_api): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_api.check_sensors.call_count == 1 + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 2 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == { + (f"{device.name} Temperature", "22.5"), + (f"{device.name} Humidity", "43.7"), + } + + +async def test_rm4_pro_hts2_sensor_update(hass): + """Test a successful RM4 pro sensor update with HTS2 cable.""" + device = get_device("Garage") + mock_api = device.get_mock_api() + mock_api.check_sensors.return_value = {"temperature": 16.7, "humidity": 34.1} + mock_entry = device.get_mock_entry() + mock_entry.add_to_hass(hass) + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + with patch("broadlink.gendevice", return_value=mock_api): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 2 + + mock_api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0} + await hass.helpers.entity_component.async_update_entity( + next(iter(sensors)).entity_id + ) + assert mock_api.check_sensors.call_count == 2 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == { + (f"{device.name} Temperature", "16.8"), + (f"{device.name} Humidity", "34.0"), + } + + +async def test_rm4_pro_no_sensor(hass): + """Test we do not set up sensors for RM4 pro without HTS2 cable.""" + device = get_device("Garage") + mock_api = device.get_mock_api() + mock_api.check_sensors.return_value = {"temperature": 0, "humidity": 0} + mock_entry = device.get_mock_entry() + mock_entry.add_to_hass(hass) + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + with patch("broadlink.gendevice", return_value=mock_api): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert mock_api.check_sensors.call_count <= 1 + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + assert len(sensors) == 0 From b759f2c7cad549c70fb1cb31aa5cb8a8bf26b44f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 27 Aug 2020 12:45:17 +0200 Subject: [PATCH 368/862] Updated certifi to > 2020.6.20 (#39160) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7b73b9b9b00..757f1b56fe6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2020.4.5.1 +certifi>=2020.6.20 ciso8601==2.1.3 cryptography==2.9.2 defusedxml==0.6.0 diff --git a/requirements.txt b/requirements.txt index b7da4348d02..baa48241a06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2020.4.5.1 +certifi>=2020.6.20 ciso8601==2.1.3 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/setup.py b/setup.py index bb57c4fdf7b..0bbdf9f05a8 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ REQUIRES = [ "async_timeout==3.0.1", "attrs==19.3.0", "bcrypt==3.1.7", - "certifi>=2020.4.5.1", + "certifi>=2020.6.20", "ciso8601==2.1.3", "importlib-metadata==1.6.0;python_version<'3.8'", "jinja2>=2.11.2", From b0a2c8d430e008a29a84676585c5593a8790e88b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 27 Aug 2020 13:26:46 +0200 Subject: [PATCH 369/862] Bump hangups to 0.4.10 (#39312) --- homeassistant/components/hangouts/manifest.json | 4 +++- homeassistant/components/hangouts/services.yaml | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 6eb62c3f590..80eed48cde9 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -3,6 +3,8 @@ "name": "Google Hangouts", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", - "requirements": ["hangups==0.4.9"], + "requirements": [ + "hangups==0.4.10" + ], "codeowners": [] } diff --git a/homeassistant/components/hangouts/services.yaml b/homeassistant/components/hangouts/services.yaml index ff11762235d..717e2888493 100644 --- a/homeassistant/components/hangouts/services.yaml +++ b/homeassistant/components/hangouts/services.yaml @@ -9,10 +9,10 @@ send_message: example: '[{"id": "UgxrXzVrARmjx_C6AZx4AaABAagBo-6UCw"}, {"name": "Test Conversation"}]' message: description: List of message segments, only the "text" field is required in every segment. [Required] - example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]' + example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}]' data: description: Other options ['image_file' / 'image_url'] - example: '{ "image_file": "file" } or { "image_url": "url" }' + example: '{ "image_file": "file" }' reconnect: description: Reconnect the bot. diff --git a/requirements_all.txt b/requirements_all.txt index 39e6f82e6d4..f7386e3e41b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -714,7 +714,7 @@ ha-philipsjs==0.0.8 habitipy==0.2.0 # homeassistant.components.hangouts -hangups==0.4.9 +hangups==0.4.10 # homeassistant.components.cloud hass-nabucasa==0.35.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4474cb79c6..0e05ae58e71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -350,7 +350,7 @@ griddypower==0.1.0 ha-ffmpeg==2.0 # homeassistant.components.hangouts -hangups==0.4.9 +hangups==0.4.10 # homeassistant.components.cloud hass-nabucasa==0.35.0 From 0d7eec710c82c3e2403b0258f27dd2c05b01d4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gabriel?= Date: Thu, 27 Aug 2020 08:51:18 -0300 Subject: [PATCH 370/862] Fix Panasonic Viera config flow and state update (#39303) --- homeassistant/components/panasonic_viera/__init__.py | 4 ---- homeassistant/components/panasonic_viera/config_flow.py | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 24b07835fbd..30b9190d4d1 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -178,9 +178,6 @@ class Remote: self.muted = self._control.get_mute() self.volume = self._control.get_volume() / 100 - self.state = STATE_ON - self.available = True - async def async_send_key(self, key): """Send a key to the TV and handle exceptions.""" try: @@ -231,7 +228,6 @@ class Remote: except (TimeoutError, URLError, SOAPError, OSError): self.state = STATE_OFF self.available = self._on_action is not None - await self.async_create_remote_control() except Exception as err: # pylint: disable=broad-except _LOGGER.exception("An unknown error occurred: %s", err) self.state = STATE_OFF diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index bcaa3bb090d..2416f01baf3 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -95,7 +95,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: pin = user_input[CONF_PIN] try: - self._remote.authorize_pin_code(pincode=pin) + await self.hass.async_add_executor_job( + partial(self._remote.authorize_pin_code, pincode=pin) + ) except SOAPError as err: _LOGGER.error("Invalid PIN code: %s", err) errors["base"] = ERROR_INVALID_PIN_CODE @@ -119,7 +121,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) try: - self._remote.request_pin_code(name="Home Assistant") + await self.hass.async_add_executor_job( + partial(self._remote.request_pin_code, name="Home Assistant") + ) except (TimeoutError, URLError, SOAPError, OSError) as err: _LOGGER.error("The remote connection was lost: %s", err) return self.async_abort(reason=REASON_NOT_CONNECTED) From 1c2ebdf307c5925f34db8688888a188c876f670c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 27 Aug 2020 13:56:20 +0200 Subject: [PATCH 371/862] Upgrade black to 20.8b1 (#39287) --- .pre-commit-config.yaml | 2 +- homeassistant/auth/__init__.py | 4 +- homeassistant/auth/models.py | 5 +- homeassistant/bootstrap.py | 15 ++- homeassistant/components/acmeda/helpers.py | 3 +- .../components/airvisual/__init__.py | 3 +- homeassistant/components/airvisual/sensor.py | 8 +- homeassistant/components/almond/__init__.py | 6 +- .../components/androidtv/media_player.py | 3 +- .../components/arcam_fmj/config_flow.py | 3 +- .../components/arcam_fmj/media_player.py | 5 +- .../components/august/config_flow.py | 8 +- homeassistant/components/august/gateway.py | 4 +- homeassistant/components/august/sensor.py | 6 +- homeassistant/components/avri/config_flow.py | 4 +- homeassistant/components/awair/__init__.py | 4 +- homeassistant/components/awair/sensor.py | 12 +- homeassistant/components/axis/light.py | 4 +- .../components/azure_devops/__init__.py | 12 +- homeassistant/components/blink/__init__.py | 3 +- homeassistant/components/blink/config_flow.py | 11 +- .../components/broadlink/config_flow.py | 4 +- homeassistant/components/brother/__init__.py | 5 +- homeassistant/components/bsblan/climate.py | 5 +- .../components/cert_expiry/__init__.py | 5 +- .../components/cert_expiry/config_flow.py | 3 +- .../components/channels/media_player.py | 12 +- homeassistant/components/cloud/http_api.py | 5 +- homeassistant/components/control4/light.py | 6 +- .../components/coolmaster/__init__.py | 5 +- homeassistant/components/cover/__init__.py | 3 +- .../components/daikin/config_flow.py | 12 +- homeassistant/components/decora/light.py | 3 +- homeassistant/components/demo/config_flow.py | 8 +- .../components/devolo_home_control/cover.py | 4 +- homeassistant/components/directv/__init__.py | 4 +- .../components/directv/config_flow.py | 3 +- .../components/directv/media_player.py | 8 +- homeassistant/components/directv/remote.py | 8 +- homeassistant/components/eafm/config_flow.py | 3 +- homeassistant/components/elgato/__init__.py | 6 +- .../components/elgato/config_flow.py | 6 +- homeassistant/components/elgato/light.py | 5 +- homeassistant/components/elkm1/__init__.py | 4 +- homeassistant/components/emulated_hue/upnp.py | 5 +- homeassistant/components/fibaro/climate.py | 4 +- homeassistant/components/flume/__init__.py | 6 +- .../components/flunearyou/__init__.py | 4 +- .../components/forked_daapd/config_flow.py | 3 +- homeassistant/components/freebox/__init__.py | 4 +- .../components/freebox/config_flow.py | 3 +- .../components/geniushub/__init__.py | 3 +- homeassistant/components/gios/config_flow.py | 3 +- homeassistant/components/glances/sensor.py | 3 +- .../components/google_assistant/helpers.py | 6 +- homeassistant/components/group/cover.py | 5 +- homeassistant/components/guardian/switch.py | 4 +- homeassistant/components/harmony/remote.py | 4 +- .../components/hisense_aehw4a1/__init__.py | 3 +- homeassistant/components/history/__init__.py | 3 +- .../components/home_connect/__init__.py | 6 +- .../components/home_connect/switch.py | 4 +- .../homeassistant/triggers/state.py | 6 +- homeassistant/components/homekit/__init__.py | 13 ++- .../components/homekit/config_flow.py | 3 +- homeassistant/components/homekit/img_util.py | 4 +- .../components/homekit/type_cameras.py | 17 ++- .../components/homekit/type_humidifiers.py | 24 ++-- .../components/homekit/type_media_players.py | 11 +- .../components/homekit/type_thermostats.py | 5 +- .../components/homekit_controller/climate.py | 3 +- .../homekit_controller/connection.py | 3 +- .../homekit_controller/media_player.py | 3 +- homeassistant/components/honeywell/climate.py | 7 +- homeassistant/components/http/ban.py | 5 +- homeassistant/components/http/view.py | 9 +- .../components/huawei_lte/__init__.py | 8 +- homeassistant/components/huawei_lte/sensor.py | 11 +- homeassistant/components/hue/__init__.py | 6 +- homeassistant/components/hue/sensor_base.py | 4 +- homeassistant/components/image/__init__.py | 8 +- homeassistant/components/influxdb/__init__.py | 5 +- homeassistant/components/influxdb/const.py | 3 +- homeassistant/components/insteon/__init__.py | 3 +- .../components/insteon/config_flow.py | 7 +- .../components/insteon/insteon_entity.py | 5 +- homeassistant/components/ipp/__init__.py | 5 +- homeassistant/components/ipp/config_flow.py | 3 +- homeassistant/components/iqvia/__init__.py | 4 +- homeassistant/components/knx/__init__.py | 5 +- homeassistant/components/knx/factory.py | 4 +- homeassistant/components/kodi/__init__.py | 4 +- homeassistant/components/kodi/config_flow.py | 3 +- .../components/konnected/config_flow.py | 3 +- homeassistant/components/konnected/panel.py | 15 ++- homeassistant/components/light/__init__.py | 3 +- homeassistant/components/lock/__init__.py | 3 +- homeassistant/components/lovelace/__init__.py | 3 +- .../components/meteo_france/__init__.py | 7 +- .../components/meteo_france/config_flow.py | 3 +- .../components/meteo_france/sensor.py | 3 +- .../components/meteo_france/weather.py | 3 +- homeassistant/components/metoffice/weather.py | 8 +- homeassistant/components/mikrotik/hub.py | 5 +- homeassistant/components/mill/config_flow.py | 15 ++- .../components/mobile_app/webhook.py | 11 +- .../components/monoprice/config_flow.py | 5 +- homeassistant/components/mqtt/__init__.py | 26 ++++- homeassistant/components/mqtt/climate.py | 3 +- homeassistant/components/mqtt/config_flow.py | 8 +- .../components/mqtt/device_trigger.py | 10 +- homeassistant/components/mysensors/gateway.py | 3 +- homeassistant/components/nad/media_player.py | 6 +- homeassistant/components/netatmo/__init__.py | 6 +- homeassistant/components/netatmo/api.py | 4 +- homeassistant/components/netatmo/camera.py | 17 ++- homeassistant/components/netatmo/climate.py | 11 +- .../components/netatmo/config_flow.py | 13 ++- .../components/netatmo/data_handler.py | 7 +- homeassistant/components/netatmo/light.py | 13 ++- homeassistant/components/netatmo/sensor.py | 8 +- .../components/netgear/device_tracker.py | 6 +- homeassistant/components/nexia/scene.py | 4 +- homeassistant/components/nexia/sensor.py | 7 +- homeassistant/components/nuki/lock.py | 11 +- .../components/numato/binary_sensor.py | 8 +- homeassistant/components/numato/switch.py | 8 +- homeassistant/components/nut/config_flow.py | 7 +- .../components/nx584/alarm_control_panel.py | 7 +- .../components/openuv/config_flow.py | 4 +- homeassistant/components/ovo_energy/sensor.py | 3 +- homeassistant/components/ozw/services.py | 4 +- homeassistant/components/ozw/websocket_api.py | 6 +- .../components/panasonic_viera/__init__.py | 8 +- .../components/panasonic_viera/config_flow.py | 6 +- .../components/panel_custom/__init__.py | 12 +- homeassistant/components/pi_hole/__init__.py | 7 +- homeassistant/components/plex/server.py | 11 +- .../components/plum_lightpad/config_flow.py | 4 +- .../components/powerwall/__init__.py | 4 +- .../components/prometheus/__init__.py | 5 +- homeassistant/components/rachio/entity.py | 12 +- homeassistant/components/rachio/switch.py | 4 +- .../components/radiotherm/climate.py | 3 +- homeassistant/components/rainbird/switch.py | 5 +- homeassistant/components/recorder/__init__.py | 3 +- homeassistant/components/recorder/purge.py | 6 +- homeassistant/components/recorder/util.py | 4 +- homeassistant/components/rfxtrx/__init__.py | 4 +- .../components/rfxtrx/binary_sensor.py | 4 +- homeassistant/components/rfxtrx/cover.py | 4 +- homeassistant/components/rfxtrx/light.py | 4 +- homeassistant/components/rfxtrx/sensor.py | 4 +- homeassistant/components/rfxtrx/switch.py | 4 +- homeassistant/components/ring/__init__.py | 7 +- homeassistant/components/ring/camera.py | 5 +- homeassistant/components/ring/config_flow.py | 8 +- homeassistant/components/risco/__init__.py | 5 +- homeassistant/components/roku/__init__.py | 14 ++- homeassistant/components/roku/config_flow.py | 7 +- homeassistant/components/roku/media_player.py | 4 +- homeassistant/components/roomba/__init__.py | 4 +- .../components/samsungtv/config_flow.py | 5 +- .../components/samsungtv/media_player.py | 14 ++- homeassistant/components/sense/sensor.py | 8 +- .../components/shelly/config_flow.py | 3 +- .../components/simplisafe/config_flow.py | 12 +- homeassistant/components/smappee/__init__.py | 6 +- .../components/smappee/binary_sensor.py | 8 +- homeassistant/components/smappee/sensor.py | 4 +- homeassistant/components/smappee/switch.py | 4 +- homeassistant/components/smarthab/__init__.py | 4 +- .../components/smartthings/__init__.py | 3 +- .../components/smartthings/config_flow.py | 3 +- homeassistant/components/sms/__init__.py | 4 +- homeassistant/components/sms/sensor.py | 13 ++- homeassistant/components/somfy/__init__.py | 6 +- homeassistant/components/somfy/api.py | 4 +- .../components/sonarr/config_flow.py | 4 +- homeassistant/components/songpal/__init__.py | 4 +- .../components/sonos/media_player.py | 5 +- .../components/speedtestdotnet/__init__.py | 5 +- .../components/spider/config_flow.py | 9 +- .../components/squeezebox/media_player.py | 4 +- homeassistant/components/stream/hls.py | 3 +- homeassistant/components/stream/worker.py | 6 +- .../components/surepetcare/binary_sensor.py | 5 +- .../components/syncthru/config_flow.py | 6 +- .../components/synology_dsm/__init__.py | 9 +- homeassistant/components/tado/__init__.py | 15 ++- homeassistant/components/tado/climate.py | 4 +- homeassistant/components/tado/water_heater.py | 4 +- homeassistant/components/template/cover.py | 3 +- homeassistant/components/template/fan.py | 3 +- homeassistant/components/template/vacuum.py | 5 +- .../components/tensorflow/image_processing.py | 7 +- homeassistant/components/tesla/__init__.py | 5 +- .../components/tesla/binary_sensor.py | 3 +- homeassistant/components/tesla/climate.py | 3 +- .../components/tesla/device_tracker.py | 3 +- homeassistant/components/tesla/lock.py | 3 +- .../components/tibber/config_flow.py | 13 ++- .../components/totalconnect/__init__.py | 4 +- homeassistant/components/tplink/light.py | 8 +- .../components/transmission/config_flow.py | 4 +- .../components/transmission/sensor.py | 4 +- homeassistant/components/tuya/__init__.py | 3 +- homeassistant/components/tuya/climate.py | 5 +- homeassistant/components/tuya/cover.py | 5 +- homeassistant/components/tuya/fan.py | 5 +- homeassistant/components/tuya/light.py | 5 +- homeassistant/components/tuya/scene.py | 5 +- homeassistant/components/tuya/switch.py | 5 +- homeassistant/components/unifi/controller.py | 4 +- homeassistant/components/upnp/__init__.py | 3 +- homeassistant/components/upnp/config_flow.py | 18 ++- homeassistant/components/vera/__init__.py | 4 +- homeassistant/components/vera/config_flow.py | 11 +- .../components/vizio/media_player.py | 10 +- homeassistant/components/waqi/sensor.py | 14 ++- homeassistant/components/wemo/__init__.py | 12 +- homeassistant/components/wemo/fan.py | 5 +- .../components/wilight/parent_device.py | 5 +- .../components/wirelesstag/__init__.py | 3 +- homeassistant/components/withings/common.py | 16 ++- homeassistant/components/withings/sensor.py | 5 +- homeassistant/components/wled/__init__.py | 10 +- homeassistant/components/worldclock/sensor.py | 9 +- .../components/xiaomi_aqara/binary_sensor.py | 14 ++- .../components/xiaomi_miio/remote.py | 8 +- homeassistant/components/xs1/__init__.py | 3 +- .../components/yamaha/media_player.py | 4 +- homeassistant/components/zha/config_flow.py | 12 +- .../components/zha/core/channels/hvac.py | 5 +- homeassistant/components/zone/__init__.py | 8 +- homeassistant/config.py | 4 +- homeassistant/config_entries.py | 20 ++-- homeassistant/core.py | 4 +- homeassistant/data_entry_flow.py | 5 +- homeassistant/helpers/aiohttp_client.py | 4 +- homeassistant/helpers/entity.py | 4 +- homeassistant/helpers/entity_component.py | 6 +- homeassistant/helpers/entity_platform.py | 4 +- homeassistant/helpers/instance_id.py | 4 +- homeassistant/helpers/reload.py | 3 +- homeassistant/helpers/script.py | 4 +- homeassistant/helpers/storage.py | 7 +- homeassistant/helpers/translation.py | 8 +- homeassistant/runner.py | 4 +- homeassistant/setup.py | 4 +- homeassistant/util/timeout.py | 5 +- homeassistant/util/unit_system.py | 4 +- homeassistant/util/yaml/loader.py | 4 +- requirements_test_pre_commit.txt | 2 +- .../config_flow/tests/test_config_flow.py | 3 +- .../integration/__init__.py | 6 +- script/translations/clean.py | 5 +- .../abode/test_alarm_control_panel.py | 3 +- .../accuweather/test_config_flow.py | 28 +++-- tests/components/adguard/test_config_flow.py | 12 +- tests/components/agent_dvr/__init__.py | 4 +- .../components/agent_dvr/test_config_flow.py | 6 +- .../components/airvisual/test_config_flow.py | 6 +- tests/components/alexa/test_smart_home.py | 11 +- tests/components/almond/test_config_flow.py | 8 +- tests/components/almond/test_init.py | 3 +- .../components/androidtv/test_media_player.py | 5 +- .../components/arcam_fmj/test_config_flow.py | 28 +++-- .../arcam_fmj/test_device_trigger.py | 8 +- tests/components/atag/test_climate.py | 3 +- tests/components/atag/test_config_flow.py | 21 +++- tests/components/august/test_config_flow.py | 9 +- tests/components/august/test_init.py | 7 +- tests/components/august/test_lock.py | 3 +- tests/components/auth/test_indieauth.py | 3 +- tests/components/automation/test_init.py | 6 +- tests/components/avri/test_config_flow.py | 3 +- tests/components/awair/test_config_flow.py | 27 +++-- tests/components/axis/test_config_flow.py | 15 ++- tests/components/axis/test_device.py | 6 +- .../azure_devops/test_config_flow.py | 27 +++-- tests/components/binary_sensor/test_init.py | 6 +- tests/components/blebox/test_cover.py | 5 +- tests/components/blebox/test_light.py | 30 ++++- tests/components/blebox/test_switch.py | 30 ++++- tests/components/blink/test_config_flow.py | 24 ++-- tests/components/bond/common.py | 3 +- tests/components/bond/test_config_flow.py | 8 +- tests/components/bond/test_fan.py | 10 +- tests/components/bond/test_init.py | 9 +- tests/components/bond/test_light.py | 5 +- .../components/broadlink/test_config_flow.py | 108 ++++++++++++------ tests/components/bsblan/__init__.py | 4 +- tests/components/bsblan/test_config_flow.py | 6 +- tests/components/camera/test_init.py | 6 +- .../cast/test_home_assistant_cast.py | 9 +- tests/components/cast/test_media_player.py | 6 +- tests/components/cloud/test_http_api.py | 3 +- tests/components/command_line/test_cover.py | 9 +- tests/components/command_line/test_sensor.py | 4 +- .../components/config/test_config_entries.py | 3 +- tests/components/config/test_core.py | 3 +- tests/components/config/test_customize.py | 3 +- tests/components/conftest.py | 3 +- tests/components/control4/test_config_flow.py | 6 +- .../components/coolmaster/test_config_flow.py | 3 +- .../coronavirus/test_config_flow.py | 3 +- tests/components/daikin/test_config_flow.py | 31 +++-- tests/components/datadog/test_init.py | 5 +- tests/components/debugpy/test_init.py | 4 +- tests/components/deconz/test_config_flow.py | 24 ++-- tests/components/deconz/test_gateway.py | 9 +- tests/components/denonavr/test_config_flow.py | 53 ++++++--- .../components/denonavr/test_media_player.py | 7 +- .../device_sun_light_trigger/test_init.py | 4 +- .../devolo_home_control/test_config_flow.py | 6 +- tests/components/dexcom/test_config_flow.py | 40 +++++-- tests/components/dexcom/test_init.py | 6 +- tests/components/dexcom/test_sensor.py | 3 +- tests/components/dialogflow/test_init.py | 3 +- tests/components/directv/test_config_flow.py | 41 +++++-- tests/components/doorbird/test_config_flow.py | 21 ++-- tests/components/dunehd/test_config_flow.py | 8 +- tests/components/dynalite/test_init.py | 31 +++-- tests/components/efergy/test_sensor.py | 6 +- tests/components/elgato/__init__.py | 4 +- tests/components/elgato/test_config_flow.py | 6 +- tests/components/elgato/test_light.py | 9 +- tests/components/elkm1/test_config_flow.py | 30 +++-- tests/components/enocean/test_config_flow.py | 6 +- tests/components/esphome/test_config_flow.py | 4 +- tests/components/filter/test_sensor.py | 9 +- .../flick_electric/test_config_flow.py | 10 +- tests/components/flume/test_config_flow.py | 18 ++- .../components/flunearyou/test_config_flow.py | 3 +- .../forked_daapd/test_config_flow.py | 4 +- .../forked_daapd/test_media_player.py | 4 +- .../garmin_connect/test_config_flow.py | 4 +- tests/components/gdacs/test_sensor.py | 20 +++- tests/components/geofency/test_init.py | 3 +- tests/components/gogogate2/common.py | 3 +- .../components/gogogate2/test_config_flow.py | 3 +- tests/components/gogogate2/test_cover.py | 12 +- .../google_assistant/test_helpers.py | 6 +- .../components/google_assistant/test_init.py | 4 +- .../google_assistant/test_smart_home.py | 3 +- .../components/google_assistant/test_trait.py | 98 ++++++++++++---- tests/components/gpslogger/test_init.py | 3 +- tests/components/griddy/test_config_flow.py | 9 +- tests/components/group/common.py | 22 +++- tests/components/group/test_light.py | 9 +- tests/components/guardian/test_config_flow.py | 3 +- tests/components/harmony/test_config_flow.py | 46 +++++--- tests/components/hassio/conftest.py | 6 +- tests/components/hassio/test_discovery.py | 3 +- tests/components/hassio/test_init.py | 3 +- tests/components/hisense_aehw4a1/test_init.py | 3 +- tests/components/history/test_init.py | 3 +- tests/components/hlk_sw16/test_config_flow.py | 23 ++-- tests/components/homeassistant/test_init.py | 11 +- .../triggers/test_homeassistant.py | 3 +- tests/components/homekit/test_config_flow.py | 44 ++++--- .../homekit/test_get_accessories.py | 3 +- tests/components/homekit/test_homekit.py | 3 +- tests/components/homekit/test_type_cameras.py | 45 +++++++- tests/components/homekit/test_type_covers.py | 10 +- tests/components/homekit/test_type_fans.py | 5 +- .../homekit/test_type_humidifiers.py | 26 +++-- .../test_homeassistant_bridge.py | 6 +- .../test_simpleconnect_fan.py | 6 +- .../components/homekit_controller/test_fan.py | 10 +- .../homematicip_cloud/test_binary_sensor.py | 9 +- .../homematicip_cloud/test_climate.py | 3 +- .../components/homematicip_cloud/test_init.py | 4 +- tests/components/http/test_forwarded.py | 6 +- tests/components/hue/test_config_flow.py | 32 ++++-- tests/components/hue/test_init.py | 12 +- .../test_config_flow.py | 15 ++- .../hvv_departures/test_config_flow.py | 51 ++++++--- .../components/image_processing/test_init.py | 3 +- tests/components/influxdb/test_init.py | 9 +- tests/components/influxdb/test_sensor.py | 14 ++- tests/components/insteon/test_config_flow.py | 12 +- tests/components/ipma/test_config_flow.py | 4 +- tests/components/ipp/test_config_flow.py | 78 +++++++++---- .../islamic_prayer_times/test_config_flow.py | 6 +- .../islamic_prayer_times/test_init.py | 15 ++- tests/components/isy994/test_config_flow.py | 48 +++++--- tests/components/izone/test_config_flow.py | 3 +- tests/components/kodi/test_config_flow.py | 102 ++++++++++++----- tests/components/kodi/test_device_trigger.py | 13 ++- .../components/konnected/test_config_flow.py | 6 +- tests/components/konnected/test_init.py | 15 ++- tests/components/light/test_device_action.py | 5 +- tests/components/light/test_init.py | 20 +++- tests/components/local_ip/test_config_flow.py | 5 +- tests/components/locative/test_init.py | 3 +- .../lutron_caseta/test_config_flow.py | 6 +- tests/components/mailgun/test_init.py | 6 +- tests/components/melissa/test_init.py | 3 +- .../meteo_france/test_config_flow.py | 36 ++++-- .../components/metoffice/test_config_flow.py | 12 +- tests/components/metoffice/test_sensor.py | 24 +++- tests/components/metoffice/test_weather.py | 40 +++++-- tests/components/mikrotik/test_init.py | 18 ++- tests/components/mill/test_config_flow.py | 8 +- .../minecraft_server/test_config_flow.py | 24 ++-- tests/components/mobile_app/test_webhook.py | 10 +- .../components/monoprice/test_config_flow.py | 3 +- .../components/monoprice/test_media_player.py | 12 +- tests/components/moon/test_sensor.py | 5 +- .../mqtt/test_alarm_control_panel.py | 70 +++++++++--- tests/components/mqtt/test_common.py | 54 +++++++-- tests/components/mqtt/test_config_flow.py | 6 +- tests/components/mqtt/test_init.py | 9 +- tests/components/mqtt/test_light_json.py | 8 +- .../components/mqtt_eventstream/test_init.py | 3 +- tests/components/myq/test_config_flow.py | 15 ++- tests/components/myq/util.py | 3 +- tests/components/netatmo/test_config_flow.py | 10 +- tests/components/nexia/test_config_flow.py | 15 ++- tests/components/nexia/util.py | 3 +- tests/components/nightscout/__init__.py | 15 ++- .../components/nightscout/test_config_flow.py | 16 ++- tests/components/nightscout/test_init.py | 3 +- tests/components/nuheat/test_climate.py | 12 +- tests/components/nuheat/test_init.py | 3 +- tests/components/nut/test_config_flow.py | 51 ++++++--- tests/components/nut/util.py | 3 +- tests/components/nws/test_config_flow.py | 21 ++-- tests/components/nws/test_init.py | 5 +- tests/components/nws/test_weather.py | 40 +++++-- tests/components/onvif/test_config_flow.py | 10 +- .../opentherm_gw/test_config_flow.py | 27 +++-- tests/components/openuv/test_config_flow.py | 9 +- .../components/ovo_energy/test_config_flow.py | 9 +- .../components/owntracks/test_config_flow.py | 9 +- tests/components/ozw/test_config_flow.py | 3 +- .../panasonic_viera/test_config_flow.py | 56 ++++++--- tests/components/panasonic_viera/test_init.py | 19 ++- tests/components/pi_hole/test_config_flow.py | 13 ++- tests/components/plant/test_init.py | 3 +- tests/components/plex/test_config_flow.py | 42 ++++--- tests/components/plex/test_init.py | 5 +- tests/components/plugwise/test_config_flow.py | 19 ++- .../plum_lightpad/test_config_flow.py | 6 +- tests/components/plum_lightpad/test_init.py | 3 +- .../components/poolsense/test_config_flow.py | 3 +- .../powerwall/test_binary_sensor.py | 3 +- .../components/powerwall/test_config_flow.py | 15 ++- tests/components/powerwall/test_sensor.py | 3 +- tests/components/ps4/test_config_flow.py | 7 +- tests/components/push/test_camera.py | 6 +- tests/components/qwikswitch/test_init.py | 4 +- tests/components/rachio/test_config_flow.py | 9 +- .../rainmachine/test_config_flow.py | 9 +- tests/components/rest/test_sensor.py | 12 +- tests/components/rfxtrx/conftest.py | 7 +- tests/components/ring/test_config_flow.py | 3 +- .../risco/test_alarm_control_panel.py | 12 +- tests/components/risco/test_binary_sensor.py | 21 +++- tests/components/risco/test_config_flow.py | 17 ++- tests/components/rmvtransport/test_sensor.py | 12 +- tests/components/roku/__init__.py | 6 +- tests/components/roku/test_config_flow.py | 23 ++-- tests/components/roku/test_init.py | 3 +- tests/components/roomba/test_config_flow.py | 12 +- tests/components/roon/test_config_flow.py | 18 ++- .../components/samsungtv/test_config_flow.py | 27 +++-- .../components/samsungtv/test_media_player.py | 4 +- tests/components/sense/test_config_flow.py | 3 +- tests/components/sentry/test_config_flow.py | 21 ++-- tests/components/shelly/test_config_flow.py | 40 +++++-- .../signal_messenger/test_notify.py | 12 +- .../components/simplisafe/test_config_flow.py | 11 +- tests/components/smappee/test_config_flow.py | 42 ++++--- .../components/smart_meter_texas/conftest.py | 9 +- .../smart_meter_texas/test_config_flow.py | 18 ++- tests/components/smarthab/test_config_flow.py | 3 +- tests/components/smartthings/conftest.py | 3 +- .../smartthings/test_config_flow.py | 21 +++- tests/components/smartthings/test_init.py | 3 +- tests/components/smhi/test_weather.py | 4 +- tests/components/solarlog/test_config_flow.py | 3 +- tests/components/sonarr/__init__.py | 18 ++- tests/components/sonarr/test_config_flow.py | 28 +++-- tests/components/sonarr/test_init.py | 3 +- tests/components/songpal/test_config_flow.py | 29 +++-- tests/components/sonos/test_media_player.py | 3 +- .../soundtouch/test_media_player.py | 42 +++++-- .../speedtestdotnet/test_config_flow.py | 16 ++- tests/components/speedtestdotnet/test_init.py | 20 +++- tests/components/spider/test_config_flow.py | 6 +- .../components/squeezebox/test_config_flow.py | 24 ++-- tests/components/statistics/test_sensor.py | 9 +- tests/components/syncthru/test_config_flow.py | 4 +- .../synology_dsm/test_config_flow.py | 6 +- tests/components/tado/test_config_flow.py | 21 ++-- tests/components/tado/util.py | 6 +- .../template/test_alarm_control_panel.py | 4 +- tests/components/template/test_init.py | 45 ++++++-- tests/components/template/test_sensor.py | 3 +- tests/components/template/test_switch.py | 6 +- tests/components/tibber/test_config_flow.py | 4 +- tests/components/tile/test_config_flow.py | 3 +- tests/components/toon/test_config_flow.py | 3 +- tests/components/tplink/test_init.py | 3 +- tests/components/tplink/test_light.py | 30 ++++- tests/components/traccar/test_init.py | 3 +- tests/components/tradfri/test_config_flow.py | 5 +- tests/components/tts/test_init.py | 3 +- tests/components/tuya/test_config_flow.py | 6 +- tests/components/twitch/test_twitch.py | 15 ++- tests/components/unifi/test_controller.py | 3 +- tests/components/unifi/test_device_tracker.py | 52 ++++++--- tests/components/unifi/test_sensor.py | 13 ++- tests/components/unifi/test_switch.py | 16 ++- .../components/universal/test_media_player.py | 9 +- tests/components/upnp/test_config_flow.py | 13 ++- tests/components/vera/test_climate.py | 16 ++- tests/components/vera/test_cover.py | 16 ++- tests/components/vera/test_light.py | 16 ++- tests/components/vera/test_lock.py | 8 +- tests/components/vera/test_scene.py | 7 +- tests/components/vera/test_switch.py | 8 +- tests/components/vilfo/test_config_flow.py | 6 +- tests/components/vizio/conftest.py | 3 +- tests/components/vizio/test_media_player.py | 21 +++- tests/components/volumio/test_config_flow.py | 36 ++++-- tests/components/webhook/test_init.py | 3 +- tests/components/webostv/test_media_player.py | 4 +- tests/components/wiffi/test_config_flow.py | 9 +- tests/components/wilight/__init__.py | 4 +- tests/components/wilight/test_config_flow.py | 7 +- tests/components/wilight/test_init.py | 3 +- tests/components/wilight/test_light.py | 15 ++- tests/components/withings/common.py | 3 +- .../components/withings/test_binary_sensor.py | 4 +- tests/components/withings/test_common.py | 12 +- tests/components/withings/test_config_flow.py | 5 +- tests/components/withings/test_sensor.py | 8 +- tests/components/wled/test_config_flow.py | 6 +- tests/components/wled/test_light.py | 78 +++++++++---- tests/components/wolflink/test_config_flow.py | 6 +- .../xiaomi_aqara/test_config_flow.py | 42 ++++--- .../xiaomi_miio/test_config_flow.py | 14 ++- tests/components/zeroconf/test_usage.py | 6 +- tests/components/zerproc/test_config_flow.py | 24 +++- tests/components/zha/test_climate.py | 12 +- tests/components/zha/test_config_flow.py | 12 +- tests/components/zha/test_cover.py | 5 +- tests/components/zha/test_device.py | 30 ++++- tests/components/zha/test_discover.py | 5 +- tests/components/zha/test_sensor.py | 4 +- tests/components/zwave/test_init.py | 15 ++- tests/helpers/test_condition.py | 91 ++++++++------- tests/helpers/test_config_entry_flow.py | 3 +- .../helpers/test_config_entry_oauth2_flow.py | 17 ++- tests/helpers/test_entity_component.py | 4 +- tests/helpers/test_entity_registry.py | 5 +- tests/helpers/test_frame.py | 6 +- tests/helpers/test_location.py | 10 +- tests/helpers/test_network.py | 60 ++++++---- tests/helpers/test_reload.py | 13 ++- tests/helpers/test_service.py | 21 +++- tests/helpers/test_state.py | 4 +- tests/helpers/test_template.py | 75 ++++++++---- tests/ignore_uncaught_exceptions.py | 5 +- tests/test_bootstrap.py | 3 +- tests/test_config.py | 58 ++++++---- tests/test_config_entries.py | 26 +++-- tests/test_requirements.py | 3 +- tests/test_setup.py | 6 +- tests/util/test_json.py | 22 ++-- 574 files changed, 4389 insertions(+), 1725 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2aec5453923..29365969725 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 19.10b0 + rev: 20.8b1 hooks: - id: black args: diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 26bd10535d0..5a39090ae44 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -312,9 +312,7 @@ class AuthManager: if provider is not None and hasattr(provider, "async_will_remove_credentials"): # https://github.com/python/mypy/issues/1424 - await provider.async_will_remove_credentials( # type: ignore - credentials - ) + await provider.async_will_remove_credentials(credentials) # type: ignore await self._store.async_remove_credentials(credentials) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index e4f31eea330..5a838cfc805 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -48,7 +48,10 @@ class User: ) _permissions: Optional[perm_mdl.PolicyPermissions] = attr.ib( - init=False, eq=False, order=False, default=None, + init=False, + eq=False, + order=False, + default=None, ) @property diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a7953cbec6c..1a8c1d554e3 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -112,7 +112,8 @@ async def async_setup_hass( config_dict = await conf_util.async_hass_config_yaml(hass) except HomeAssistantError as err: _LOGGER.error( - "Failed to parse configuration.yaml: %s. Activating safe mode", err, + "Failed to parse configuration.yaml: %s. Activating safe mode", + err, ) else: if not is_virtual_env(): @@ -160,7 +161,8 @@ async def async_setup_hass( http_conf = (await http.async_get_last_config(hass)) or {} await async_from_config_dict( - {"safe_mode": {}, "http": http_conf}, hass, + {"safe_mode": {}, "http": http_conf}, + hass, ) if runtime_config.open_ui: @@ -331,8 +333,10 @@ def async_enable_logging( ): if log_rotate_days: - err_handler: logging.FileHandler = logging.handlers.TimedRotatingFileHandler( - err_log_path, when="midnight", backupCount=log_rotate_days + err_handler: logging.FileHandler = ( + logging.handlers.TimedRotatingFileHandler( + err_log_path, when="midnight", backupCount=log_rotate_days + ) ) else: err_handler = logging.FileHandler(err_log_path, mode="w", delay=True) @@ -391,7 +395,8 @@ async def _async_log_pending_setups( if remaining: _LOGGER.warning( - "Waiting on integrations to complete setup: %s", ", ".join(remaining), + "Waiting on integrations to complete setup: %s", + ", ".join(remaining), ) diff --git a/homeassistant/components/acmeda/helpers.py b/homeassistant/components/acmeda/helpers.py index f8ea744be77..cec971e5fdd 100644 --- a/homeassistant/components/acmeda/helpers.py +++ b/homeassistant/components/acmeda/helpers.py @@ -37,5 +37,6 @@ async def update_devices(hass, config_entry, api): ) if device is not None: dev_registry.async_update_device( - device.id, name=api_item.name, + device.id, + name=api_item.name, ) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 8d41bd359b8..3449d5e865f 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -220,7 +220,8 @@ async def async_setup_entry(hass, config_entry): ) else: api_coro = client.api.nearest_city( - config_entry.data[CONF_LATITUDE], config_entry.data[CONF_LONGITUDE], + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], ) try: diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index b122f3c27b4..0fbb9ec9b38 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -98,7 +98,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY: sensors = [ AirVisualGeographySensor( - coordinator, config_entry, kind, name, icon, unit, locale, + coordinator, + config_entry, + kind, + name, + icon, + unit, + locale, ) for locale in GEOGRAPHY_SENSOR_LOCALES for kind, name, icon, unit in GEOGRAPHY_SENSORS diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 3710ff14b1a..2117f809b04 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -108,8 +108,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEnt auth = AlmondLocalAuth(entry.data["host"], websession) else: # OAuth2 - implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) ) oauth_session = config_entry_oauth2_flow.OAuth2Session( hass, entry, implementation diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index e2468247a72..0d39d528cca 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -597,7 +597,8 @@ class ADBDevice(MediaPlayerEntity): msg = f"Output from service '{SERVICE_LEARN_SENDEVENT}' from {self.entity_id}: '{output}'" self.hass.components.persistent_notification.async_create( - msg, title="Android TV", + msg, + title="Android TV", ) _LOGGER.info("%s", msg) diff --git a/homeassistant/components/arcam_fmj/config_flow.py b/homeassistant/components/arcam_fmj/config_flow.py index d6cf1c02d3b..2a9f1946cb4 100644 --- a/homeassistant/components/arcam_fmj/config_flow.py +++ b/homeassistant/components/arcam_fmj/config_flow.py @@ -42,7 +42,8 @@ class ArcamFmjFlowHandler(config_entries.ConfigFlow): await client.stop() return self.async_create_entry( - title=f"{DEFAULT_NAME} ({host})", data={CONF_HOST: host, CONF_PORT: port}, + title=f"{DEFAULT_NAME} ({host})", + data={CONF_HOST: host, CONF_PORT: port}, ) async def async_step_user(self, user_input=None): diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 0ead1f16b94..6b5b5856190 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -60,7 +60,10 @@ class ArcamFmj(MediaPlayerEntity): """Representation of a media device.""" def __init__( - self, device_name, state: State, uuid: str, + self, + device_name, + state: State, + uuid: str, ): """Initialize device.""" self._state = state diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index acdfb1d4b63..bf6f1d9cd81 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -30,7 +30,9 @@ DATA_SCHEMA = vol.Schema( async def async_validate_input( - hass: core.HomeAssistant, data, august_gateway, + hass: core.HomeAssistant, + data, + august_gateway, ): """Validate the user input allows us to connect. @@ -89,7 +91,9 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await async_validate_input( - self.hass, user_input, self._august_gateway, + self.hass, + user_input, + self._august_gateway, ) await self.async_set_unique_id(user_input[CONF_USERNAME]) return self.async_create_entry(title=info["title"], data=info["data"]) diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index bb39523a984..272c50ac02a 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -122,8 +122,8 @@ class AugustGateway: """Refresh the august access token if needed.""" if self.authenticator.should_refresh(): async with self._token_refresh_lock: - refreshed_authentication = await self.authenticator.async_refresh_access_token( - force=False + refreshed_authentication = ( + await self.authenticator.async_refresh_access_token(force=False) ) _LOGGER.info( "Refreshed august access token. The old token expired at %s, and the new token expires at %s", diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 3276f8b073b..c8f2704da8d 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -70,7 +70,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) continue _LOGGER.debug( - "Adding battery sensor for %s", device.device_name, + "Adding battery sensor for %s", + device.device_name, ) devices.append(AugustBatterySensor(data, "device_battery", device, device)) @@ -84,7 +85,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) continue _LOGGER.debug( - "Adding keypad battery sensor for %s", device.device_name, + "Adding keypad battery sensor for %s", + device.device_name, ) keypad_battery_sensor = AugustBatterySensor( data, "linked_keypad_battery", detail.keypad, device diff --git a/homeassistant/components/avri/config_flow.py b/homeassistant/components/avri/config_flow.py index d6f9dbf7b62..987b3679b3c 100644 --- a/homeassistant/components/avri/config_flow.py +++ b/homeassistant/components/avri/config_flow.py @@ -32,7 +32,9 @@ class AvriConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _show_setup_form(self, errors=None): """Show the setup form to the user.""" return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors or {}, + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors or {}, ) async def async_step_user(self, user_input=None): diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index c002693d6e9..2ae436bebba 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -96,7 +96,9 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): if not matching_flows: self.hass.async_create_task( self.hass.config_entries.flow.async_init( - DOMAIN, context=flow_context, data=self._config_entry.data, + DOMAIN, + context=flow_context, + data=self._config_entry.data, ) ) diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index e4e2f3fbbd6..28802454aa2 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -32,7 +32,8 @@ from .const import ( ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_ACCESS_TOKEN): cv.string}, extra=vol.ALLOW_EXTRA, + {vol.Required(CONF_ACCESS_TOKEN): cv.string}, + extra=vol.ALLOW_EXTRA, ) @@ -43,7 +44,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) ) @@ -83,7 +86,10 @@ class AwairSensor(Entity): """Defines an Awair sensor entity.""" def __init__( - self, kind: str, device: AwairDevice, coordinator: AwairDataUpdateCoordinator, + self, + kind: str, + device: AwairDevice, + coordinator: AwairDataUpdateCoordinator, ) -> None: """Set up an individual AwairSensor.""" self._kind = kind diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index 75b2d59e5f5..3ae15a46244 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -54,8 +54,8 @@ class AxisLight(AxisEventBase, LightEntity): def get_light_capabilities(): """Get light capabilities.""" - current_intensity = self.device.api.vapix.light_control.get_current_intensity( - self.light_id + current_intensity = ( + self.device.api.vapix.light_control.get_current_intensity(self.light_id) ) self.current_intensity = current_intensity["data"]["intensity"] diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 00f08496dd3..e08dd4d8559 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -38,7 +38,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool ) hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=entry.data, + DOMAIN, + context={"source": "reauth"}, + data=entry.data, ) ) return False @@ -115,7 +117,13 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): def device_info(self) -> Dict[str, Any]: """Return device information about this Azure DevOps instance.""" return { - "identifiers": {(DOMAIN, self.organization, self.project,)}, + "identifiers": { + ( + DOMAIN, + self.organization, + self.project, + ) + }, "manufacturer": self.organization, "name": self.project, } diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index e2c43a27547..0809018522e 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -103,7 +103,8 @@ async def async_setup_entry(hass, entry): """Call blink to send new pin.""" pin = call.data[CONF_PIN] hass.data[DOMAIN][entry.entry_id].auth.send_auth_key( - hass.data[DOMAIN][entry.entry_id], pin, + hass.data[DOMAIN][entry.entry_id], + pin, ) hass.services.async_register(DOMAIN, SERVICE_REFRESH, blink_refresh) diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index 3073a093261..63c822cfd1f 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -86,7 +86,9 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } return self.async_show_form( - step_id="user", data_schema=vol.Schema(data_schema), errors=errors, + step_id="user", + data_schema=vol.Schema(data_schema), + errors=errors, ) async def async_step_2fa(self, user_input=None): @@ -156,7 +158,12 @@ class BlinkOptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form( step_id="simple_options", data_schema=vol.Schema( - {vol.Optional(CONF_SCAN_INTERVAL, default=scan_interval,): int} + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=scan_interval, + ): int + } ), ) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 899a277b1c3..284a9bffe19 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -107,7 +107,9 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, } return self.async_show_form( - step_id="user", data_schema=vol.Schema(data_schema), errors=errors, + step_id="user", + data_schema=vol.Schema(data_schema), + errors=errors, ) async def async_step_auth(self): diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 5daf54a568c..4c69282a996 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -71,7 +71,10 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator): self.brother = Brother(host, kind=kind) super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, ) async def _async_update_data(self): diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index aaeb1fbffdb..c84b3304c7b 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -86,7 +86,10 @@ class BSBLanClimate(ClimateEntity): """Defines a BSBLan climate device.""" def __init__( - self, entry_id: str, bsblan: BSBLan, info: Info, + self, + entry_id: str, + bsblan: BSBLan, + info: Info, ): """Initialize BSBLan climate device.""" self._current_temperature: Optional[float] = None diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 19fa4927d05..84629353bce 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -65,7 +65,10 @@ class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime]): name = f"{self.host}{display_port}" super().__init__( - hass, _LOGGER, name=name, update_interval=SCAN_INTERVAL, + hass, + _LOGGER, + name=name, + update_interval=SCAN_INTERVAL, ) async def _async_update_data(self) -> Optional[datetime]: diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index e23d832bb20..7953a7bb8cf 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -60,7 +60,8 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title_port = f":{port}" if port != DEFAULT_PORT else "" title = f"{host}{title_port}" return self.async_create_entry( - title=title, data={CONF_HOST: host, CONF_PORT: port}, + title=title, + data={CONF_HOST: host, CONF_PORT: port}, ) if ( # pylint: disable=no-member self.context["source"] == config_entries.SOURCE_IMPORT diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 78286b70145..54dc0f18374 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -69,13 +69,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_SEEK_FORWARD, {}, "seek_forward", + SERVICE_SEEK_FORWARD, + {}, + "seek_forward", ) platform.async_register_entity_service( - SERVICE_SEEK_BACKWARD, {}, "seek_backward", + SERVICE_SEEK_BACKWARD, + {}, + "seek_backward", ) platform.async_register_entity_service( - SERVICE_SEEK_BY, {vol.Required(ATTR_SECONDS): vol.Coerce(int)}, "seek_by", + SERVICE_SEEK_BY, + {vol.Required(ATTR_SECONDS): vol.Coerce(int)}, + "seek_by", ) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 6710d8682e2..6589f2b43f5 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -72,7 +72,10 @@ _CLOUD_ERRORS = { "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.", ), asyncio.TimeoutError: (502, "Unable to reach the Home Assistant cloud."), - aiohttp.ClientError: (HTTP_INTERNAL_SERVER_ERROR, "Error making internal request",), + aiohttp.ClientError: ( + HTTP_INTERNAL_SERVER_ERROR, + "Error making internal request", + ), } diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index 36727b21ecf..2871da34f11 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -36,7 +36,8 @@ async def async_setup_entry( entry_data = hass.data[DOMAIN][entry.entry_id] scan_interval = entry_data[CONF_SCAN_INTERVAL] _LOGGER.debug( - "Scan interval = %s", scan_interval, + "Scan interval = %s", + scan_interval, ) async def async_update_data_non_dimmer(): @@ -95,7 +96,8 @@ async def async_setup_entry( continue except KeyError: _LOGGER.exception( - "Unknown device properties received from Control4: %s", item, + "Unknown device properties received from Control4: %s", + item, ) continue diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 390b807f5cf..14165bd93b3 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -61,7 +61,10 @@ class CoolmasterDataUpdateCoordinator(DataUpdateCoordinator): self._coolmaster = coolmaster super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, ) async def _async_update_data(self): diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index ef17abc8a40..f1039505f82 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -338,5 +338,6 @@ class CoverDevice(CoverEntity): """Print deprecation warning.""" super().__init_subclass__(**kwargs) _LOGGER.warning( - "CoverDevice is deprecated, modify %s to extend CoverEntity", cls.__name__, + "CoverDevice is deprecated, modify %s to extend CoverEntity", + cls.__name__, ) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 65e48c7ee6f..7142771a7c3 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -85,17 +85,23 @@ class FlowHandler(config_entries.ConfigFlow): ) except web_exceptions.HTTPForbidden: return self.async_show_form( - step_id="user", data_schema=self.schema, errors={"base": "forbidden"}, + step_id="user", + data_schema=self.schema, + errors={"base": "forbidden"}, ) except ClientError: _LOGGER.exception("ClientError") return self.async_show_form( - step_id="user", data_schema=self.schema, errors={"base": "device_fail"}, + step_id="user", + data_schema=self.schema, + errors={"base": "device_fail"}, ) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error creating device") return self.async_show_form( - step_id="user", data_schema=self.schema, errors={"base": "device_fail"}, + step_id="user", + data_schema=self.schema, + errors={"base": "device_fail"}, ) mac = device.mac diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 5b6015b7c54..b9bf09f0c6d 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -62,7 +62,8 @@ def retry(method): return method(device, *args, **kwargs) except (decora.decoraException, AttributeError, BTLEException): _LOGGER.warning( - "Decora connect error for device %s. Reconnecting...", device.name, + "Decora connect error for device %s. Reconnecting...", + device.name, ) # pylint: disable=protected-access device._switch.connect() diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index 0899ca5dbb9..35f72e9e0cd 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -59,7 +59,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): default=self.config_entry.options.get(CONF_BOOLEAN, False), ): bool, vol.Optional( - CONF_INT, default=self.config_entry.options.get(CONF_INT, 10), + CONF_INT, + default=self.config_entry.options.get(CONF_INT, 10), ): int, } ), @@ -77,7 +78,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_STRING, - default=self.config_entry.options.get(CONF_STRING, "Default",), + default=self.config_entry.options.get( + CONF_STRING, + "Default", + ), ): str, vol.Optional( CONF_SELECT, diff --git a/homeassistant/components/devolo_home_control/cover.py b/homeassistant/components/devolo_home_control/cover.py index 3e05d4f20b4..b93713cc700 100644 --- a/homeassistant/components/devolo_home_control/cover.py +++ b/homeassistant/components/devolo_home_control/cover.py @@ -50,8 +50,8 @@ class DevoloCoverDeviceEntity(DevoloDeviceEntity, CoverEntity): sync=self._sync, ) - self._multi_level_switch_property = device_instance.multi_level_switch_property.get( - element_uid + self._multi_level_switch_property = ( + device_instance.multi_level_switch_property.get(element_uid) ) self._position = self._multi_level_switch_property.value diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 677487945be..d01f8bd25c9 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -44,7 +44,9 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: for entry_config in config[DOMAIN]: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=entry_config, ) ) diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 406f2628ee4..d84c3925ac4 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -121,7 +121,8 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_create_entry( - title=self.discovery_info[CONF_NAME], data=self.discovery_info, + title=self.discovery_info[CONF_NAME], + data=self.discovery_info, ) def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 205503fe17f..acf30cb103a 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -70,7 +70,9 @@ async def async_setup_entry( for location in dtv.device.locations: entities.append( DIRECTVMediaPlayer( - dtv=dtv, name=str.title(location.name), address=location.address, + dtv=dtv, + name=str.title(location.name), + address=location.address, ) ) @@ -83,7 +85,9 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: """Initialize DirecTV media player.""" super().__init__( - dtv=dtv, name=name, address=address, + dtv=dtv, + name=name, + address=address, ) self._assumed_state = None diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index 9665b0aea17..64695ae3813 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -29,7 +29,9 @@ async def async_setup_entry( for location in dtv.device.locations: entities.append( DIRECTVRemote( - dtv=dtv, name=str.title(location.name), address=location.address, + dtv=dtv, + name=str.title(location.name), + address=location.address, ) ) @@ -42,7 +44,9 @@ class DIRECTVRemote(DIRECTVEntity, RemoteEntity): def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: """Initialize DirecTV remote.""" super().__init__( - dtv=dtv, name=name, address=address, + dtv=dtv, + name=name, + address=address, ) self._available = False diff --git a/homeassistant/components/eafm/config_flow.py b/homeassistant/components/eafm/config_flow.py index 0f640951f9f..f1cf60bb97e 100644 --- a/homeassistant/components/eafm/config_flow.py +++ b/homeassistant/components/eafm/config_flow.py @@ -32,7 +32,8 @@ class UKFloodsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(station, raise_on_progress=False) self._abort_if_unique_id_configured() return self.async_create_entry( - title=user_input["station"], data={"station": station}, + title=user_input["station"], + data={"station": station}, ) session = async_get_clientsession(hass=self.hass) diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index 993748033b5..31f8c2b59cf 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -24,7 +24,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elgato Key Light from a config entry.""" session = async_get_clientsession(hass) - elgato = Elgato(entry.data[CONF_HOST], port=entry.data[CONF_PORT], session=session,) + elgato = Elgato( + entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + session=session, + ) # Ensure we can connect to it try: diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index 8411980ee44..687310c2c3e 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -132,5 +132,9 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): async def _get_elgato_info(self, host: str, port: int) -> Info: """Get device information from an Elgato Key Light device.""" session = async_get_clientsession(self.hass) - elgato = Elgato(host, port=port, session=session,) + elgato = Elgato( + host, + port=port, + session=session, + ) return await elgato.info() diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 9dae0cd1f40..313b5600248 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -49,7 +49,10 @@ class ElgatoLight(LightEntity): """Defines a Elgato Key Light.""" def __init__( - self, entry_id: str, elgato: Elgato, info: Info, + self, + entry_id: str, + elgato: Elgato, + info: Info, ): """Initialize Elgato Key Light.""" self._brightness: Optional[int] = None diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 946e40b7e23..dbb45ec317f 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -173,7 +173,9 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=conf, ) ) diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index bc4def242c3..58f964d4984 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -57,7 +57,10 @@ class DescriptionXmlView(HomeAssistantView): @core.callback def create_upnp_datagram_endpoint( - host_ip_addr, upnp_bind_multicast, advertise_ip, advertise_port, + host_ip_addr, + upnp_bind_multicast, + advertise_ip, + advertise_port, ): """Create the UPNP socket and protocol.""" diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 191185c4a2a..5776a293756 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -146,8 +146,8 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._unit_of_temp = TEMP_CELSIUS if self._fan_mode_device: - fan_modes = self._fan_mode_device.fibaro_device.properties.supportedModes.split( - "," + fan_modes = ( + self._fan_mode_device.fibaro_device.properties.supportedModes.split(",") ) for mode in fan_modes: mode = int(mode) diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index cc618f64b4e..513f77edfb2 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -61,7 +61,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) flume_devices = await hass.async_add_executor_job( - partial(FlumeDeviceList, flume_auth, http_session=http_session,) + partial( + FlumeDeviceList, + flume_auth, + http_session=http_session, + ) ) except RequestException: raise ConfigEntryNotReady diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 6e1c8ddb3d2..5e7ebd90cf1 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -184,7 +184,9 @@ class FluNearYouData: # If this is the first registration we have, start a time interval: if not self._async_cancel_time_interval_listener: self._async_cancel_time_interval_listener = async_track_time_interval( - self._hass, self._async_update_listener_action, DEFAULT_SCAN_INTERVAL, + self._hass, + self._async_update_listener_action, + DEFAULT_SCAN_INTERVAL, ) api_category = async_get_api_category(sensor_type) diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index 45ea8861d4a..faef33f8334 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -176,7 +176,8 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if entry.data.get(CONF_HOST) != discovery_info["host"]: continue self.hass.config_entries.async_update_entry( - entry, title=discovery_info["properties"]["Machine Name"], + entry, + title=discovery_info["properties"]["Machine Name"], ) return self.async_abort(reason="already_configured") diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 9e303c75e7a..9120c7d0866 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -50,7 +50,9 @@ async def async_setup(hass, config): for freebox_conf in conf: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=freebox_conf, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=freebox_conf, ) ) diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py index 9cef6aa0c38..0589dfb2ef1 100644 --- a/homeassistant/components/freebox/config_flow.py +++ b/homeassistant/components/freebox/config_flow.py @@ -82,7 +82,8 @@ class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await fbx.close() return self.async_create_entry( - title=self._host, data={CONF_HOST: self._host, CONF_PORT: self._port}, + title=self._host, + data={CONF_HOST: self._host, CONF_PORT: self._port}, ) except AuthorizationError as error: diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 0e779ebdcba..909de81521c 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -87,7 +87,8 @@ SET_ZONE_OVERRIDE_SCHEMA = vol.Schema( vol.Coerce(float), vol.Range(min=4, max=28) ), vol.Optional(ATTR_DURATION): vol.All( - cv.time_period, vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), + cv.time_period, + vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), ), } ) diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index 5741af47a07..dfef829c479 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -44,7 +44,8 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await gios.update() return self.async_create_entry( - title=user_input[CONF_STATION_ID], data=user_input, + title=user_input[CONF_STATION_ID], + data=user_input, ) except (ApiError, ClientConnectorError, asyncio.TimeoutError): errors["base"] = "cannot_connect" diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index f701dfdb741..deb8650cd4f 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -157,7 +157,8 @@ class GlancesSensor(Entity): self._state = round(disk["free"] / 1024 ** 3, 1) except KeyError: self._state = round( - (disk["size"] - disk["used"]) / 1024 ** 3, 1, + (disk["size"] - disk["used"]) / 1024 ** 3, + 1, ) elif self.type == "sensor_temp": for sensor in value["sensors"]: diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 49e08e90e4b..8ec2bce4602 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -208,7 +208,11 @@ class AbstractConfig(ABC): return webhook.async_register( - self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook, + self.hass, + DOMAIN, + "Local Support", + webhook_id, + self._handle_local_webhook, ) self._local_sdk_active = True diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 6a2111747d6..b0d0b8b7bd2 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -103,7 +103,10 @@ class CoverGroup(GroupEntity, CoverEntity): ) async def async_update_supported_features( - self, entity_id: str, new_state: Optional[State], update_state: bool = True, + self, + entity_id: str, + new_state: Optional[State], + update_state: bool = True, ) -> None: """Update dictionaries with supported features.""" if not new_state: diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 6822fea6de2..8aebd078e04 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -156,7 +156,9 @@ class GuardianSwitch(GuardianEntity, SwitchEntity): try: async with self._client: await self._client.system.upgrade_firmware( - url=url, port=port, filename=filename, + url=url, + port=port, + filename=filename, ) except GuardianError as err: LOGGER.error("Error during service call: %s", err) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index fe2f0535308..ff7825013e8 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -122,7 +122,9 @@ async def async_setup_entry( platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_SYNC, HARMONY_SYNC_SCHEMA, "sync", + SERVICE_SYNC, + HARMONY_SYNC_SCHEMA, + "sync", ) platform.async_register_entity_service( SERVICE_CHANGE_CHANNEL, HARMONY_CHANGE_CHANNEL_SCHEMA, "change_channel" diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py index 721039d0e1c..a2412a2fed4 100644 --- a/homeassistant/components/hisense_aehw4a1/__init__.py +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -60,7 +60,8 @@ async def async_setup(hass, config): hass.data[DOMAIN] = conf hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, ) ) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 9354800b843..0e6fabb66aa 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -297,7 +297,8 @@ def _get_states_with_session( most_recent_state_ids = most_recent_state_ids.subquery() query = query.join( - most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, + most_recent_state_ids, + States.state_id == most_recent_state_ids.c.max_state_id, ).filter(~States.domain.in_(IGNORE_DOMAINS)) if filters: diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 4e575963577..38f487a98a1 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -59,8 +59,10 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Home Connect from a config entry.""" - implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) ) hc_api = api.ConfigEntryAuth(hass, entry, implementation) diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index c5fcdef25b7..346e739e5ff 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -103,7 +103,9 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): _LOGGER.debug("Tried to switch on %s", self.name) try: await self.hass.async_add_executor_job( - self.device.appliance.set_setting, BSH_POWER_STATE, BSH_POWER_ON, + self.device.appliance.set_setting, + BSH_POWER_STATE, + BSH_POWER_ON, ) except HomeConnectError as err: _LOGGER.error("Error while trying to turn on device: %s", err) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 7ccba9aade8..d51e63964e9 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -143,7 +143,11 @@ async def async_attach_trigger( return cur_value == new_value unsub_track_same[entity] = async_track_same_state( - hass, period[entity], call_action, _check_same_state, entity_ids=entity, + hass, + period[entity], + call_action, + _check_same_state, + entity_ids=entity, ) unsub = async_track_state_change_event(hass, entity_id, state_automation_listener) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 2b6db6af528..d1c909cf2b0 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -172,7 +172,9 @@ async def async_setup(hass: HomeAssistant, config: dict): conf[CONF_ENTRY_INDEX] = index hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=conf, ) ) @@ -632,14 +634,16 @@ class HomeKit: ) if motion_binary_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( - CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id, + CONF_LINKED_MOTION_SENSOR, + motion_binary_sensor_entity_id, ) doorbell_binary_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_OCCUPANCY) ) if doorbell_binary_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( - CONF_LINKED_DOORBELL_SENSOR, doorbell_binary_sensor_entity_id, + CONF_LINKED_DOORBELL_SENSOR, + doorbell_binary_sensor_entity_id, ) if state.entity_id.startswith(f"{HUMIDIFIER_DOMAIN}."): @@ -648,7 +652,8 @@ class HomeKit: ].get((SENSOR_DOMAIN, DEVICE_CLASS_HUMIDITY)) if current_humidity_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( - CONF_LINKED_HUMIDITY_SENSOR, current_humidity_sensor_entity_id, + CONF_LINKED_HUMIDITY_SENSOR, + current_humidity_sensor_entity_id, ) async def _async_set_device_info_attributes(self, ent_reg_ent, dev_reg, entity_id): diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 2d0d2df40b7..6a3206ac41b 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -261,7 +261,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): data_schema = vol.Schema( { vol.Optional( - CONF_CAMERA_COPY, default=cameras_with_copy, + CONF_CAMERA_COPY, + default=cameras_with_copy, ): cv.multi_select(self.included_cameras), } ) diff --git a/homeassistant/components/homekit/img_util.py b/homeassistant/components/homekit/img_util.py index 235cfe60df5..2baede8d957 100644 --- a/homeassistant/components/homekit/img_util.py +++ b/homeassistant/components/homekit/img_util.py @@ -27,7 +27,9 @@ def scale_jpeg_camera_image(cam_image, width, height): break return turbo_jpeg.scale_with_quality( - cam_image.content, scaling_factor=scaling_factor, quality=75, + cam_image.content, + scaling_factor=scaling_factor, + quality=75, ) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 91b13a93eca..0febd7461e5 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -220,15 +220,18 @@ class Camera(HomeAccessory, PyhapCamera): serv_doorbell = self.add_preload_service(SERV_DOORBELL) self.set_primary_service(serv_doorbell) self._char_doorbell_detected = serv_doorbell.configure_char( - CHAR_PROGRAMMABLE_SWITCH_EVENT, value=0, + CHAR_PROGRAMMABLE_SWITCH_EVENT, + value=0, ) serv_stateless_switch = self.add_preload_service( SERV_STATELESS_PROGRAMMABLE_SWITCH ) - self._char_doorbell_detected_switch = serv_stateless_switch.configure_char( - CHAR_PROGRAMMABLE_SWITCH_EVENT, - value=0, - valid_values={"SinglePress": DOORBELL_SINGLE_PRESS}, + self._char_doorbell_detected_switch = ( + serv_stateless_switch.configure_char( + CHAR_PROGRAMMABLE_SWITCH_EVENT, + value=0, + valid_values={"SinglePress": DOORBELL_SINGLE_PRESS}, + ) ) serv_speaker = self.add_preload_service(SERV_SPEAKER) serv_speaker.configure_char(CHAR_MUTE, value=0) @@ -387,7 +390,9 @@ class Camera(HomeAccessory, PyhapCamera): await self._async_ffmpeg_watch(session_info["id"]) session_info[FFMPEG_WATCHER] = async_track_time_interval( - self.hass, watch_session, FFMPEG_WATCH_INTERVAL, + self.hass, + watch_session, + FFMPEG_WATCH_INTERVAL, ) return await self._async_ffmpeg_watch(session_info["id"]) diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index f59015a392d..7175198c4b5 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -88,17 +88,21 @@ class HumidifierDehumidifier(HomeAccessory): ) # Current and target mode characteristics - self.char_current_humidifier_dehumidifier = serv_humidifier_dehumidifier.configure_char( - CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER, value=0 + self.char_current_humidifier_dehumidifier = ( + serv_humidifier_dehumidifier.configure_char( + CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER, value=0 + ) ) - self.char_target_humidifier_dehumidifier = serv_humidifier_dehumidifier.configure_char( - CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER, - value=self._hk_device_class, - valid_values={ - HC_HASS_TO_HOMEKIT_DEVICE_CLASS_NAME[ - device_class - ]: self._hk_device_class - }, + self.char_target_humidifier_dehumidifier = ( + serv_humidifier_dehumidifier.configure_char( + CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER, + value=self._hk_device_class, + valid_values={ + HC_HASS_TO_HOMEKIT_DEVICE_CLASS_NAME[ + device_class + ]: self._hk_device_class + }, + ) ) # Current and target humidity characteristics diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 91cdd25ee42..c278322f77e 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -231,7 +231,9 @@ class MediaPlayer(HomeAccessory): if self.chars[FEATURE_PLAY_STOP]: hk_state = current_state == STATE_PLAYING _LOGGER.debug( - '%s: Set current state for "play_stop" to %s', self.entity_id, hk_state, + '%s: Set current state for "play_stop" to %s', + self.entity_id, + hk_state, ) if self.chars[FEATURE_PLAY_STOP].value != hk_state: self.chars[FEATURE_PLAY_STOP].set_value(hk_state) @@ -414,7 +416,9 @@ class TelevisionMediaPlayer(HomeAccessory): if CHAR_VOLUME_SELECTOR in self.chars_speaker: current_mute_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)) _LOGGER.debug( - "%s: Set current mute state to %s", self.entity_id, current_mute_state, + "%s: Set current mute state to %s", + self.entity_id, + current_mute_state, ) if self.char_mute.value != current_mute_state: self.char_mute.set_value(current_mute_state) @@ -429,7 +433,8 @@ class TelevisionMediaPlayer(HomeAccessory): self.char_input_source.set_value(index) elif hk_state: _LOGGER.warning( - "%s: Sources out of sync. Restart Home Assistant", self.entity_id, + "%s: Sources out of sync. Restart Home Assistant", + self.entity_id, ) if self.char_input_source.value != 0: self.char_input_source.set_value(0) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 1d0d760b963..7f588af77fe 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -325,7 +325,10 @@ class Thermostat(HomeAccessory): if service: params[ATTR_ENTITY_ID] = self.entity_id self.call_service( - DOMAIN_CLIMATE, service, params, ", ".join(events), + DOMAIN_CLIMATE, + service, + params, + ", ".join(events), ) if CHAR_TARGET_HUMIDITY in char_values: diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 8551f4dddd5..546e46d67cb 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -287,7 +287,8 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): Requires SUPPORT_SWING_MODE. """ valid_values = clamp_enum_to_char( - SwingModeValues, self.service[CharacteristicsTypes.SWING_MODE], + SwingModeValues, + self.service[CharacteristicsTypes.SWING_MODE], ) return [SWING_MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values] diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index a5deb8aa9bc..6a59f98f3dc 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -212,7 +212,8 @@ class HKDevice: ) device = device_registry.async_get_or_create( - config_entry_id=self.config_entry.entry_id, **device_info, + config_entry_id=self.config_entry.entry_id, + **device_info, ) devices[accessory.aid] = device.id diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 249b8c9c3e0..2ffc794409b 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -129,7 +129,8 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): this_tv = this_accessory.services.iid(self._iid) input_sources = this_accessory.services.filter( - service_type=ServicesTypes.INPUT_SOURCE, parent_service=this_tv, + service_type=ServicesTypes.INPUT_SOURCE, + parent_service=this_tv, ) for input_source in input_sources: diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 07ac6c9b217..0e7958a3ea1 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -130,7 +130,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities( [ HoneywellUSThermostat( - client, device, cool_away_temp, heat_away_temp, username, password, + client, + device, + cool_away_temp, + heat_away_temp, + username, + password, ) for location in client.locations_by_id.values() for device in location.devices_by_id.values() diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index d17c57cbc08..979fe7981f4 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -124,8 +124,9 @@ async def process_wrong_login(request): request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1 # Supervisor IP should never be banned - if "hassio" in hass.config.components and hass.components.hassio.get_supervisor_ip() == str( - remote_addr + if ( + "hassio" in hass.config.components + and hass.components.hassio.get_supervisor_ip() == str(remote_addr) ): return diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 376465f375b..7766d5a0cb9 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -43,7 +43,9 @@ class HomeAssistantView: @staticmethod def json( - result: Any, status_code: int = HTTP_OK, headers: Optional[LooseHeaders] = None, + result: Any, + status_code: int = HTTP_OK, + headers: Optional[LooseHeaders] = None, ) -> web.Response: """Return a JSON response.""" try: @@ -114,7 +116,10 @@ def request_handler_factory(view: HomeAssistantView, handler: Callable) -> Calla raise HTTPUnauthorized() _LOGGER.debug( - "Serving %s to %s (auth: %s)", request.path, request.remote, authenticated, + "Serving %s to %s (auth: %s)", + request.path, + request.remote, + authenticated, ) try: diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 8b12cd0975e..8241655d2fa 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -195,7 +195,8 @@ class Router: _LOGGER.debug("Trying to authorize again...") if self.connection.enforce_authorized_connection(): _LOGGER.debug( - "...success, %s will be updated by a future periodic run", key, + "...success, %s will be updated by a future periodic run", + key, ) else: _LOGGER.debug("...failed") @@ -523,7 +524,10 @@ async def async_setup(hass: HomeAssistantType, config) -> bool: for service in ADMIN_SERVICES: hass.helpers.service.async_register_admin_service( - DOMAIN, service, service_handler, schema=SERVICE_SCHEMA, + DOMAIN, + service, + service_handler, + schema=SERVICE_SCHEMA, ) for url, router_config in domain_config.items(): diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 247fb3b6bdc..f547dbd2eb6 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -178,8 +178,12 @@ SENSOR_META = { name="Operator search mode", formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), ), - (KEY_NET_CURRENT_PLMN, "FullName"): dict(name="Operator name",), - (KEY_NET_CURRENT_PLMN, "Numeric"): dict(name="Operator code",), + (KEY_NET_CURRENT_PLMN, "FullName"): dict( + name="Operator name", + ), + (KEY_NET_CURRENT_PLMN, "Numeric"): dict( + name="Operator code", + ), KEY_NET_NET_MODE: dict(include=re.compile(r"^NetworkMode$", re.IGNORECASE)), (KEY_NET_NET_MODE, "NetworkMode"): dict( name="Preferred mode", @@ -197,7 +201,8 @@ SENSOR_META = { ), ), (KEY_SMS_SMS_COUNT, "LocalUnread"): dict( - name="SMS unread", icon="mdi:email-receive", + name="SMS unread", + icon="mdi:email-receive", ), } diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 1131d68baec..a99f9dd8a2a 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -44,7 +44,8 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.Schema( { vol.Optional(CONF_BRIDGES): vol.All( - cv.ensure_list, [BRIDGE_CONFIG_SCHEMA], + cv.ensure_list, + [BRIDGE_CONFIG_SCHEMA], ) } ) @@ -149,7 +150,8 @@ async def async_setup_entry( if options: hass.config_entries.async_update_entry( - entry, options={**entry.options, **options}, + entry, + options={**entry.options, **options}, ) bridge = HueBridge(hass, entry) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 93b98a7c9ce..af8986e0212 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -150,7 +150,9 @@ class SensorManager: self.bridge.hass.async_create_task( remove_devices( - self.bridge, [value.uniqueid for value in api.values()], current, + self.bridge, + [value.uniqueid for value in api.values()], + current, ) ) diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index a8110decd0a..91085ce4e64 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -41,7 +41,11 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.data[DOMAIN] = storage_collection = ImageStorageCollection(hass, image_dir) await storage_collection.async_load() collection.StorageCollectionWebsocket( - storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS, + storage_collection, + DOMAIN, + DOMAIN, + CREATE_FIELDS, + UPDATE_FIELDS, ).async_setup(hass, create_create=False) hass.http.register_view(ImageUploadView) @@ -94,7 +98,7 @@ class ImageStorageCollection(collection.StorageCollection): # Reset content uploaded_file.file.seek(0) - media_folder: pathlib.Path = (self.image_dir / data[CONF_ID]) + media_folder: pathlib.Path = self.image_dir / data[CONF_ID] media_folder.mkdir(parents=True) media_file = media_folder / "original" diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 555a268b62a..60939741894 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -179,7 +179,10 @@ INFLUX_SCHEMA = vol.All( create_influx_url, ) -CONFIG_SCHEMA = vol.Schema({DOMAIN: INFLUX_SCHEMA}, extra=vol.ALLOW_EXTRA,) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: INFLUX_SCHEMA}, + extra=vol.ALLOW_EXTRA, +) def _generate_event_to_json(conf: Dict) -> Callable[[Dict], str]: diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py index a9115c3fc68..c1b5ce3a591 100644 --- a/homeassistant/components/influxdb/const.py +++ b/homeassistant/components/influxdb/const.py @@ -129,7 +129,8 @@ RENDERING_WHERE_ERROR_MESSAGE = "Could not render where template: %s." 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.Coerce(str), + vol.In([DEFAULT_API_VERSION, API_VERSION_2]), ), vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_PATH): cv.string, diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 62032b681b8..b567179fa4f 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -109,7 +109,8 @@ async def async_import_config(hass, conf): if result["type"] == RESULT_TYPE_CREATE_ENTRY and options: entry = result["result"] hass.config_entries.async_update_entry( - entry=entry, options=options, + entry=entry, + options=options, ) return result diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index 40e8b81f440..c6045893365 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -248,7 +248,8 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): data[CONF_PASSWORD] = user_input[CONF_PASSWORD] self.hass.config_entries.async_update_entry(self.config_entry, data=data) return self.async_create_entry( - title="", data={**self.config_entry.options}, + title="", + data={**self.config_entry.options}, ) data_schema = build_hub_schema(**self.config_entry.data) return self.async_show_form( @@ -291,7 +292,9 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: options = _remove_override(user_input[CONF_ADDRESS], options) async_dispatcher_send( - self.hass, SIGNAL_REMOVE_DEVICE_OVERRIDE, user_input[CONF_ADDRESS], + self.hass, + SIGNAL_REMOVE_DEVICE_OVERRIDE, + user_input[CONF_ADDRESS], ) return self.async_create_entry(title="", data=options) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 851675513da..3bef7dd0247 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -94,7 +94,10 @@ class InsteonEntity(Entity): def async_entity_update(self, name, address, value, group): """Receive notification from transport that new data exists.""" _LOGGER.debug( - "Received update for device %s group %d value %s", address, group, value, + "Received update for device %s group %d value %s", + address, + group, + value, ) self.async_write_ha_state() diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 0e2b559d5e4..07d1258d735 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -110,7 +110,10 @@ class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]): ) super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, ) async def _async_update_data(self) -> IPPPrinter: diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 671bb2dd4cd..476756ddc61 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -177,7 +177,8 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_create_entry( - title=self.discovery_info[CONF_NAME], data=self.discovery_info, + title=self.discovery_info[CONF_NAME], + data=self.discovery_info, ) def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 3e67e2639e2..171e7977e95 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -180,7 +180,9 @@ class IQVIAData: # If this is the first registration we have, start a time interval: if not self._async_cancel_time_interval_listener: self._async_cancel_time_interval_listener = async_track_time_interval( - self._hass, self._async_update_listener_action, DEFAULT_SCAN_INTERVAL, + self._hass, + self._async_update_listener_action, + DEFAULT_SCAN_INTERVAL, ) api_category = async_get_api_category(sensor_type) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 4de801a19d1..989efc9b376 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -400,7 +400,10 @@ class KNXExposeSensor: else: _name = self.entity_id self.device = ExposeSensor( - self.xknx, name=_name, group_address=self.address, value_type=self.type, + self.xknx, + name=_name, + group_address=self.address, + value_type=self.type, ) self.xknx.devices.add(self.device) async_track_state_change_event( diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index f53a7436122..64245d61a08 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -218,7 +218,9 @@ def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor: def _create_notify(knx_module: XKNX, config: ConfigType) -> XknxNotification: """Return a KNX notification to be used within XKNX.""" return XknxNotification( - knx_module, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], + knx_module, + name=config[CONF_NAME], + group_address=config[CONF_ADDRESS], ) diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index dedb0ab09c4..f3cb15a17eb 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -57,7 +57,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryNotReady from error except InvalidAuthError as error: _LOGGER.error( - "Login to %s failed: [%s]", entry.data[CONF_HOST], error, + "Login to %s failed: [%s]", + entry.data[CONF_HOST], + error, ) return False diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index 34e08ff56e4..f10dcbb2d28 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -284,7 +284,8 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def _create_entry(self): return self.async_create_entry( - title=self._name or self._host, data=self._get_data(), + title=self._name or self._host, + data=self._get_data(), ) @callback diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 420d517234b..88888da44f8 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -349,7 +349,8 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) or "".join(random.choices(f"{string.ascii_uppercase}{string.digits}", k=20)) return self.async_create_entry( - title=KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]], data=self.data, + title=KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]], + data=self.data, ) @staticmethod diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index 3fcb51929ed..bd91bf3adbc 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -168,12 +168,20 @@ class AlarmPanel: if self.client: if self.api_version == CONF_ZONE: return await self.client.put_zone( - zone, state, momentary, times, pause, + zone, + state, + momentary, + times, + pause, ) # device endpoint uses pin number instead of zone return await self.client.put_device( - ZONE_TO_PIN[zone], state, momentary, times, pause, + ZONE_TO_PIN[zone], + state, + momentary, + times, + pause, ) except self.client.ClientError as err: @@ -208,7 +216,8 @@ class AlarmPanel: act = { CONF_ZONE: zone, CONF_NAME: entity.get( - CONF_NAME, f"Konnected {self.device_id[6:]} Actuator {zone}", + CONF_NAME, + f"Konnected {self.device_id[6:]} Actuator {zone}", ), ATTR_STATE: None, CONF_ACTIVATION: entity[CONF_ACTIVATION], diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 14a870696ce..b97e9e90248 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -469,5 +469,6 @@ class Light(LightEntity): """Print deprecation warning.""" super().__init_subclass__(**kwargs) _LOGGER.warning( - "Light is deprecated, modify %s to extend LightEntity", cls.__name__, + "Light is deprecated, modify %s to extend LightEntity", + cls.__name__, ) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index fb10580a1cc..addd5d9a257 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -145,5 +145,6 @@ class LockDevice(LockEntity): """Print deprecation warning.""" super().__init_subclass__(**kwargs) _LOGGER.warning( - "LockDevice is deprecated, modify %s to extend LockEntity", cls.__name__, + "LockDevice is deprecated, modify %s to extend LockEntity", + cls.__name__, ) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index b9ddef67768..2a8c97f3d4e 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -58,7 +58,8 @@ CONFIG_SCHEMA = vol.Schema( vol.Lower, vol.In([MODE_YAML, MODE_STORAGE]) ), vol.Optional(CONF_DASHBOARDS): cv.schema_with_slug_keys( - YAML_DASHBOARD_SCHEMA, slug_validator=url_slug, + YAML_DASHBOARD_SCHEMA, + slug_validator=url_slug, ), vol.Optional(CONF_RESOURCES): [RESOURCE_SCHEMA], } diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index e2049543ef3..152999b5574 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -33,7 +33,8 @@ SCAN_INTERVAL = timedelta(minutes=15) CITY_SCHEMA = vol.Schema({vol.Required(CONF_CITY): cv.string}) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CITY_SCHEMA]))}, extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CITY_SCHEMA]))}, + extra=vol.ALLOW_EXTRA, ) @@ -130,7 +131,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool department = coordinator_forecast.data.position.get("dept") _LOGGER.debug( - "Department corresponding to %s is %s", entry.title, department, + "Department corresponding to %s is %s", + entry.title, + department, ) if is_valid_warning_department(department): if not hass.data[DOMAIN].get(department): diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index 0854c280c16..4593a392ee3 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -74,7 +74,8 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( - title=city, data={CONF_LATITUDE: latitude, CONF_LONGITUDE: longitude}, + title=city, + data={CONF_LATITUDE: latitude, CONF_LONGITUDE: longitude}, ) async def async_step_import(self, user_input): diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 927250c6af6..178df5c992f 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -64,7 +64,8 @@ async def async_setup_entry( entities.append(MeteoFranceSensor(sensor_type, coordinator_forecast)) async_add_entities( - entities, False, + entities, + False, ) diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index e8afcea9702..6f110dbcdeb 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -47,7 +47,8 @@ async def async_setup_entry( async_add_entities( [ MeteoFranceWeather( - coordinator, entry.options.get(CONF_MODE, FORECAST_MODE_DAILY), + coordinator, + entry.options.get(CONF_MODE, FORECAST_MODE_DAILY), ) ], True, diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index f94c2a4ad7a..809943c616f 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -29,7 +29,13 @@ async def async_setup_entry( hass_data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [MetOfficeWeather(entry.data, hass_data,)], False, + [ + MetOfficeWeather( + entry.data, + hass_data, + ) + ], + False, ) diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 0b4ea0c5ea3..1dc6041b535 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -401,7 +401,10 @@ def get_api(hass, entry): try: api = librouteros.connect( - entry[CONF_HOST], entry[CONF_USERNAME], entry[CONF_PASSWORD], **kwargs, + entry[CONF_HOST], + entry[CONF_USERNAME], + entry[CONF_PASSWORD], + **kwargs, ) _LOGGER.debug("Connected to %s successfully", entry[CONF_HOST]) return api diff --git a/homeassistant/components/mill/config_flow.py b/homeassistant/components/mill/config_flow.py index 08eb0f5c536..230849cc78d 100644 --- a/homeassistant/components/mill/config_flow.py +++ b/homeassistant/components/mill/config_flow.py @@ -27,14 +27,18 @@ class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" if user_input is None: return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors={}, + step_id="user", + data_schema=DATA_SCHEMA, + errors={}, ) username = user_input[CONF_USERNAME].replace(" ", "") password = user_input[CONF_PASSWORD].replace(" ", "") mill_data_connection = Mill( - username, password, websession=async_get_clientsession(self.hass), + username, + password, + websession=async_get_clientsession(self.hass), ) errors = {} @@ -42,7 +46,9 @@ class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not await mill_data_connection.connect(): errors["connection_error"] = "connection_error" return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors, + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, ) unique_id = username @@ -51,5 +57,6 @@ class MillConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( - title=unique_id, data={CONF_USERNAME: username, CONF_PASSWORD: password}, + title=unique_id, + data={CONF_USERNAME: username, CONF_PASSWORD: password}, ) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index e7ff49f71f2..8820d08a518 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -251,7 +251,9 @@ async def webhook_stream_camera(hass, config_entry, data): if camera is None: return webhook_response( - {"success": False}, registration=config_entry.data, status=HTTP_BAD_REQUEST, + {"success": False}, + registration=config_entry.data, + status=HTTP_BAD_REQUEST, ) resp = {"mjpeg_path": "/api/camera_proxy_stream/%s" % (camera.entity_id)} @@ -342,7 +344,8 @@ async def webhook_update_registration(hass, config_entry, data): hass.config_entries.async_update_entry(config_entry, data=new_registration) return webhook_response( - safe_registration(new_registration), registration=new_registration, + safe_registration(new_registration), + registration=new_registration, ) @@ -420,7 +423,9 @@ async def webhook_register_sensor(hass, config_entry, data): async_dispatcher_send(hass, register_signal, data) return webhook_response( - {"success": True}, registration=config_entry.data, status=HTTP_CREATED, + {"success": True}, + registration=config_entry.data, + status=HTTP_CREATED, ) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 6c6bc87bf28..ddaafd01d8c 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -138,7 +138,10 @@ class MonopriceOptionsFlowHandler(config_entries.OptionsFlow): for idx, source in enumerate(SOURCES) } - return self.async_show_form(step_id="init", data_schema=vol.Schema(options),) + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(options), + ) class CannotConnect(exceptions.HomeAssistantError): diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 865f21b9d38..869dc27cfd6 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -531,7 +531,11 @@ async def async_setup_entry(hass, entry): conf = _merge_config(entry, conf) - hass.data[DATA_MQTT] = MQTT(hass, entry, conf,) + hass.data[DATA_MQTT] = MQTT( + hass, + entry, + conf, + ) await hass.data[DATA_MQTT].async_connect() @@ -620,7 +624,12 @@ class Subscription: class MQTT: """Home Assistant MQTT client.""" - def __init__(self, hass: HomeAssistantType, config_entry, conf,) -> None: + def __init__( + self, + hass: HomeAssistantType, + config_entry, + conf, + ) -> None: """Initialize Home Assistant MQTT client.""" # We don't import on the top because some integrations # should be able to optionally rely on MQTT. @@ -1177,7 +1186,9 @@ class MqttAvailability(Entity): } self._availability_sub_state = await async_subscribe_topics( - self.hass, self._availability_sub_state, topics, + self.hass, + self._availability_sub_state, + topics, ) @callback @@ -1256,7 +1267,9 @@ class MqttDiscoveryUpdate(Entity): async def discovery_callback(payload): """Handle discovery update.""" _LOGGER.info( - "Got update for entity with hash: %s '%s'", discovery_hash, payload, + "Got update for entity with hash: %s '%s'", + discovery_hash, + payload, ) old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD] debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) @@ -1291,7 +1304,10 @@ class MqttDiscoveryUpdate(Entity): if not self._removed_from_hass: discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] publish( - self.hass, discovery_topic, "", retain=True, + self.hass, + discovery_topic, + "", + retain=True, ) async def async_will_remove_from_hass(self) -> None: diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 3bd8aae9239..20ee183d693 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -170,7 +170,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( - CONF_FAN_MODE_LIST, default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], + CONF_FAN_MODE_LIST, + default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 4a5847366f7..8b1c350323c 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -196,7 +196,9 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ] = str return self.async_show_form( - step_id="broker", data_schema=vol.Schema(fields), errors=errors, + step_id="broker", + data_schema=vol.Schema(fields), + errors=errors, ) async def async_step_options(self, user_input=None): @@ -305,7 +307,9 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): fields[vol.Optional("will_retain", default=will[ATTR_RETAIN])] = bool return self.async_show_form( - step_id="options", data_schema=vol.Schema(fields), errors=errors, + step_id="options", + data_schema=vol.Schema(fields), + errors=errors, ) diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 01eb84e66e7..676252c3134 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -93,7 +93,10 @@ class TriggerInstance: if self.remove: self.remove() self.remove = await mqtt_trigger.async_attach_trigger( - self.trigger.hass, mqtt_config, self.action, self.automation_info, + self.trigger.hass, + mqtt_config, + self.action, + self.automation_info, ) @@ -256,7 +259,10 @@ async def async_device_removed(hass: HomeAssistant, device_id: str): clear_discovery_hash(hass, discovery_hash) device_trigger.remove_signal() mqtt.publish( - hass, discovery_topic, "", retain=True, + hass, + discovery_topic, + "", + retain=True, ) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index d906c306dfc..56c1562eea0 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -73,7 +73,8 @@ async def setup_gateways(hass, config): for index, gateway_conf in enumerate(conf[CONF_GATEWAYS]): persistence_file = gateway_conf.get( - CONF_PERSISTENCE_FILE, hass.config.path(f"mysensors{index + 1}.pickle"), + CONF_PERSISTENCE_FILE, + hass.config.path(f"mysensors{index + 1}.pickle"), ) ready_gateway = await _get_gateway(hass, config, gateway_conf, persistence_file) if ready_gateway is not None: diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 6145a89a594..a98e4eafe2d 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -66,11 +66,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NAD platform.""" if config.get(CONF_TYPE) in ("RS232", "Telnet"): add_entities( - [NAD(config)], True, + [NAD(config)], + True, ) else: add_entities( - [NADtcp(config)], True, + [NADtcp(config)], + True, ) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 0995511abcc..f68683a152d 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -83,8 +83,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Netatmo from a config entry.""" - implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) ) # Set unique id if non was set (migration) diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index b8b259ed5c1..6c5aee818d5 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -26,7 +26,9 @@ class ConfigEntryNetatmoAuth(pyatmo.auth.NetatmoOAuth2): ) super().__init__(token=self.session.token) - def refresh_tokens(self,) -> dict: + def refresh_tokens( + self, + ) -> dict: """Refresh and return new Netatmo tokens using Home Assistant OAuth2 session.""" run_coroutine_threadsafe( self.session.async_ensure_token_valid(), self.hass.loop diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 39f6839d331..bb46e25af3f 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -107,7 +107,12 @@ class NetatmoCamera(NetatmoBase, Camera): """Representation of a Netatmo camera.""" def __init__( - self, data_handler, camera_id, camera_type, home_id, quality, + self, + data_handler, + camera_id, + camera_type, + home_id, + quality, ): """Set up for access to the Netatmo camera images.""" Camera.__init__(self) @@ -172,7 +177,9 @@ class NetatmoCamera(NetatmoBase, Camera): ) elif self._vpnurl: response = requests.get( - f"{self._vpnurl}/live/snapshot_720.jpg", timeout=10, verify=True, + f"{self._vpnurl}/live/snapshot_720.jpg", + timeout=10, + verify=True, ) else: _LOGGER.error("Welcome/Presence VPN URL is None") @@ -283,12 +290,14 @@ class NetatmoCamera(NetatmoBase, Camera): if person_id is not None: self._data.set_persons_away( - person_id=person_id, home_id=self._home_id, + person_id=person_id, + home_id=self._home_id, ) _LOGGER.debug("Set %s as away", person) else: self._data.set_persons_away( - person_id=person_id, home_id=self._home_id, + person_id=person_id, + home_id=self._home_id, ) _LOGGER.debug("Set home as empty") diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index c23f8f47bef..f24591fe954 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -337,12 +337,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Set new preset mode.""" if self.target_temperature == 0: self._home_status.set_room_thermpoint( - self._id, STATE_NETATMO_HOME, + self._id, + STATE_NETATMO_HOME, ) if preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self._model == NA_VALVE: self._home_status.set_room_thermpoint( - self._id, STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, + self._id, + STATE_NETATMO_MANUAL, + DEFAULT_MAX_TEMP, ) elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]: self._home_status.set_room_thermpoint( @@ -393,7 +396,9 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Turn the entity off.""" if self._model == NA_VALVE: self._home_status.set_room_thermpoint( - self._id, STATE_NETATMO_MANUAL, DEFAULT_MIN_TEMP, + self._id, + STATE_NETATMO_MANUAL, + DEFAULT_MIN_TEMP, ) elif self.hvac_mode != HVAC_MODE_OFF: self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_OFF) diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index c78a5169d3b..6ae1fd864df 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -111,13 +111,16 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): data_schema = vol.Schema( { vol.Optional( - CONF_WEATHER_AREAS, default=weather_areas, + CONF_WEATHER_AREAS, + default=weather_areas, ): cv.multi_select(weather_areas), vol.Optional(CONF_NEW_AREA): str, } ) return self.async_show_form( - step_id="public_weather_areas", data_schema=data_schema, errors=errors, + step_id="public_weather_areas", + data_schema=data_schema, + errors=errors, ) async def async_step_public_weather(self, user_input=None): @@ -169,10 +172,12 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): ), ): cv.longitude, vol.Required( - CONF_PUBLIC_MODE, default=orig_options.get(CONF_PUBLIC_MODE, "avg"), + CONF_PUBLIC_MODE, + default=orig_options.get(CONF_PUBLIC_MODE, "avg"), ): vol.In(["avg", "max"]), vol.Required( - CONF_SHOW_ON_MAP, default=orig_options.get(CONF_SHOW_ON_MAP, False), + CONF_SHOW_ON_MAP, + default=orig_options.get(CONF_SHOW_ON_MAP, False), ): bool, } ) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 5354e7cec75..c9be4237229 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -70,7 +70,9 @@ class NetatmoDataHandler: self.listeners.append( async_dispatcher_connect( - self.hass, f"signal-{DOMAIN}-webhook-None", self.handle_event, + self.hass, + f"signal-{DOMAIN}-webhook-None", + self.handle_event, ) ) @@ -113,7 +115,8 @@ class NetatmoDataHandler: """Fetch data and notify.""" try: self.data[data_class_entry] = await self.hass.async_add_executor_job( - partial(data_class, **kwargs), self._auth, + partial(data_class, **kwargs), + self._auth, ) for update_callback in self._data_classes[data_class_entry][ "subscriptions" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index dea56e54c09..a41dae33641 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -59,7 +59,10 @@ async def async_setup_entry(hass, entry, async_add_entities): _LOGGER.debug("Adding camera light %s %s", camera["id"], camera["name"]) entities.append( NetatmoLight( - data_handler, camera["id"], camera["type"], camera["home_id"], + data_handler, + camera["id"], + camera["type"], + camera["home_id"], ) ) @@ -132,14 +135,18 @@ class NetatmoLight(NetatmoBase, LightEntity): """Turn camera floodlight on.""" _LOGGER.debug("Turn camera '%s' on", self._name) self._data.set_state( - home_id=self._home_id, camera_id=self._id, floodlight="on", + home_id=self._home_id, + camera_id=self._id, + floodlight="on", ) def turn_off(self, **kwargs): """Turn camera floodlight into auto mode.""" _LOGGER.debug("Turn camera '%s' off", self._name) self._data.set_state( - home_id=self._home_id, camera_id=self._id, floodlight="auto", + home_id=self._home_id, + camera_id=self._id, + floodlight="auto", ) @callback diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 3b704e00508..4dadd101e39 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -128,7 +128,9 @@ async def async_setup_entry(hass, entry, async_add_entities): continue _LOGGER.debug( - "Adding module %s %s", module.get("module_name"), module.get("_id"), + "Adding module %s %s", + module.get("module_name"), + module.get("_id"), ) conditions = [ c.lower() @@ -179,7 +181,9 @@ async def async_setup_entry(hass, entry, async_add_entities): if update: async_dispatcher_send( - hass, f"netatmo-config-{area.area_name}", area, + hass, + f"netatmo-config-{area.area_name}", + area, ) continue diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 5f18553ba62..f30277086de 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -71,7 +71,11 @@ class NetgearDeviceScanner(DeviceScanner): """Queries a Netgear wireless router using the SOAP-API.""" def __init__( - self, api, devices, excluded_devices, accesspoints, + self, + api, + devices, + excluded_devices, + accesspoints, ): """Initialize the scanner.""" self.tracked_devices = devices diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index 12689f05538..1700de3f059 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -34,7 +34,9 @@ class NexiaAutomationScene(NexiaEntity, Scene): def __init__(self, coordinator, automation): """Initialize the automation scene.""" super().__init__( - coordinator, name=automation.name, unique_id=automation.automation_id, + coordinator, + name=automation.name, + unique_id=automation.automation_id, ) self._automation = automation diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index abbffa2b844..ea2fd2b5718 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -126,7 +126,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Zone Status entities.append( NexiaThermostatZoneSensor( - coordinator, zone, "get_status", "Zone Status", None, None, + coordinator, + zone, + "get_status", + "Zone Status", + None, + None, ) ) # Setpoint Status diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index f7414d54802..d8585ad7458 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -51,7 +51,11 @@ LOCK_N_GO_SERVICE_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nuki lock platform.""" bridge = NukiBridge( - config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], True, DEFAULT_TIMEOUT, + config[CONF_HOST], + config[CONF_TOKEN], + config[CONF_PORT], + True, + DEFAULT_TIMEOUT, ) devices = [NukiLockEntity(lock) for lock in bridge.locks] @@ -67,7 +71,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): lock.lock_n_go(unlatch=unlatch) hass.services.register( - DOMAIN, SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA, + DOMAIN, + SERVICE_LOCK_N_GO, + service_handler, + schema=LOCK_N_GO_SERVICE_SCHEMA, ) devices.extend([NukiOpenerEntity(opener) for opener in bridge.openers]) diff --git a/homeassistant/components/numato/binary_sensor.py b/homeassistant/components/numato/binary_sensor.py index be8d3f62afa..426f072aebd 100644 --- a/homeassistant/components/numato/binary_sensor.py +++ b/homeassistant/components/numato/binary_sensor.py @@ -58,7 +58,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): continue binary_sensors.append( - NumatoGpioBinarySensor(port_name, device_id, port, invert_logic, api,) + NumatoGpioBinarySensor( + port_name, + device_id, + port, + invert_logic, + api, + ) ) add_entities(binary_sensors, True) diff --git a/homeassistant/components/numato/switch.py b/homeassistant/components/numato/switch.py index 2f1be0cf311..505d28d0c40 100644 --- a/homeassistant/components/numato/switch.py +++ b/homeassistant/components/numato/switch.py @@ -43,7 +43,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) continue switches.append( - NumatoGpioSwitch(port_name, device_id, port, invert_logic, api,) + NumatoGpioSwitch( + port_name, + device_id, + port, + invert_logic, + api, + ) ) add_entities(switches, True) diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 7cebf0c6759..33306e24acb 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -194,7 +194,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_resources() return self.async_show_form( - step_id="ups", data_schema=_ups_schema(self.ups_list), errors=errors, + step_id="ups", + data_schema=_ups_schema(self.ups_list), + errors=errors, ) async def async_step_resources(self, user_input=None): @@ -264,7 +266,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ] = cv.positive_int return self.async_show_form( - step_id="init", data_schema=vol.Schema(base_schema), + step_id="init", + data_schema=vol.Schema(base_schema), ) diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 23dbfbb090c..7ce882e3c82 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -57,7 +57,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= await hass.async_add_executor_job(alarm_client.list_zones) except requests.exceptions.ConnectionError as ex: _LOGGER.error( - "Unable to connect to %(host)s: %(reason)s", dict(host=url, reason=ex), + "Unable to connect to %(host)s: %(reason)s", + dict(host=url, reason=ex), ) raise PlatformNotReady @@ -67,7 +68,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_BYPASS_ZONE, {vol.Required(ATTR_ZONE): cv.positive_int}, "alarm_bypass", + SERVICE_BYPASS_ZONE, + {vol.Required(ATTR_ZONE): cv.positive_int}, + "alarm_bypass", ) platform.async_register_entity_service( diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index bf359ef1b30..9dfb053ff01 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -33,7 +33,9 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _show_form(self, errors=None): """Show the form to the user.""" return self.async_show_form( - step_id="user", data_schema=CONFIG_SCHEMA, errors=errors if errors else {}, + step_id="user", + data_schema=CONFIG_SCHEMA, + errors=errors if errors else {}, ) async def async_step_import(self, import_config): diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index a0781836d6c..8b0cddd3752 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -54,7 +54,8 @@ async def async_setup_entry( ) async_add_entities( - entities, True, + entities, + True, ) diff --git a/homeassistant/components/ozw/services.py b/homeassistant/components/ozw/services.py index b9281953ac9..500289cec21 100644 --- a/homeassistant/components/ozw/services.py +++ b/homeassistant/components/ozw/services.py @@ -86,7 +86,9 @@ class ZWaveServices: if payload is None: _LOGGER.error( - "Invalid value %s for parameter %s", selection, param, + "Invalid value %s for parameter %s", + selection, + param, ) return diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index 8d17b6d8d45..6e1a725b7a7 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -40,7 +40,8 @@ def websocket_get_instances(hass, connection, msg): instances.append(dict(instance.get_status().data, ozw_instance=instance.id)) connection.send_result( - msg[ID], instances, + msg[ID], + instances, ) @@ -56,7 +57,8 @@ def websocket_network_status(hass, connection, msg): manager = hass.data[DOMAIN][MANAGER] status = manager.get_instance(msg[OZW_INSTANCE]).get_status().data connection.send_result( - msg[ID], dict(status, ozw_instance=msg[OZW_INSTANCE]), + msg[ID], + dict(status, ozw_instance=msg[OZW_INSTANCE]), ) diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 30b9190d4d1..2095c5cc209 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -115,7 +115,13 @@ class Remote: """The Remote class. It stores the TV properties and the remote control connection itself.""" def __init__( - self, hass, host, port, on_action=None, app_id=None, encryption_key=None, + self, + hass, + host, + port, + on_action=None, + app_id=None, + encryption_key=None, ): """Initialize the Remote class.""" self._hass = hass diff --git a/homeassistant/components/panasonic_viera/config_flow.py b/homeassistant/components/panasonic_viera/config_flow.py index 2416f01baf3..d08e293eb45 100644 --- a/homeassistant/components/panasonic_viera/config_flow.py +++ b/homeassistant/components/panasonic_viera/config_flow.py @@ -64,7 +64,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_pairing() return self.async_create_entry( - title=self._data[CONF_NAME], data=self._data, + title=self._data[CONF_NAME], + data=self._data, ) return self.async_show_form( @@ -117,7 +118,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data = {**self._data, **encryption_data} return self.async_create_entry( - title=self._data[CONF_NAME], data=self._data, + title=self._data[CONF_NAME], + data=self._data, ) try: diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index 2cdcb8d37c4..e663665675c 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -60,9 +60,15 @@ CONFIG_SCHEMA = vol.Schema( ): cv.icon, vol.Optional(CONF_URL_PATH): cv.string, vol.Optional(CONF_CONFIG): dict, - vol.Optional(CONF_WEBCOMPONENT_PATH,): cv.string, - vol.Optional(CONF_JS_URL,): cv.string, - vol.Optional(CONF_MODULE_URL,): cv.string, + vol.Optional( + CONF_WEBCOMPONENT_PATH, + ): cv.string, + vol.Optional( + CONF_JS_URL, + ): cv.string, + vol.Optional( + CONF_MODULE_URL, + ): cv.string, vol.Optional( CONF_EMBED_IFRAME, default=DEFAULT_EMBED_IFRAME ): cv.boolean, diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 9b51cc09b35..5e7fc723cc4 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -85,7 +85,12 @@ async def async_setup_entry(hass, entry): try: session = async_get_clientsession(hass, verify_tls) api = Hole( - host, hass.loop, session, location=location, tls=use_tls, api_token=api_key, + host, + hass.loop, + session, + location=location, + tls=use_tls, + api_token=api_key, ) await api.get_data() except HoleError as ex: diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index a1bf56eb54f..2ddb5ef0a29 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -481,7 +481,8 @@ class PlexServer: return None except NotFound: _LOGGER.error( - "Playlist '%s' not found", playlist_name, + "Playlist '%s' not found", + playlist_name, ) return None @@ -581,7 +582,9 @@ class PlexServer: season = show.season(int(season_number)) except NotFound: _LOGGER.error( - "Season %d of '%s' not found", season_number, show_name, + "Season %d of '%s' not found", + season_number, + show_name, ) return None @@ -611,5 +614,7 @@ class PlexServer: _LOGGER.error("Must specify 'video_name' for this search") except NotFound: _LOGGER.error( - "Movie '%s' not found in '%s'", video_name, library_name, + "Movie '%s' not found in '%s'", + video_name, + library_name, ) diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index 6645aef02a2..c6261307d13 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -28,7 +28,9 @@ class PlumLightpadConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } return self.async_show_form( - step_id="user", data_schema=vol.Schema(schema), errors=errors or {}, + step_id="user", + data_schema=vol.Schema(schema), + errors=errors or {}, ) async def async_step_user( diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 0f25a14546d..9c1ea7afe53 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -52,7 +52,9 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=conf, ) ) return True diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 1a70e4cf78e..d9aa4bd6090 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -332,7 +332,10 @@ class PrometheusMetrics: current_action = state.attributes.get(ATTR_HVAC_ACTION) if current_action: metric = self._metric( - "climate_action", self.prometheus_cli.Gauge, "HVAC action", ["action"], + "climate_action", + self.prometheus_cli.Gauge, + "HVAC action", + ["action"], ) for action in CURRENT_HVAC_ACTIONS: metric.labels(**dict(self._labels(state), action=action)).set( diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py index 379c4e785e5..ec1807b04c2 100644 --- a/homeassistant/components/rachio/entity.py +++ b/homeassistant/components/rachio/entity.py @@ -23,9 +23,17 @@ class RachioDevice(Entity): def device_info(self): """Return the device_info of the device.""" return { - "identifiers": {(DOMAIN, self._controller.serial_number,)}, + "identifiers": { + ( + DOMAIN, + self._controller.serial_number, + ) + }, "connections": { - (device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,) + ( + device_registry.CONNECTION_NETWORK_MAC, + self._controller.mac_address, + ) }, "name": self._controller.name, "model": self._controller.model, diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 6fa992adb81..32ec6267941 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -456,7 +456,9 @@ class RachioSchedule(RachioSwitch): """Start this schedule.""" self._controller.rachio.schedulerule.start(self._schedule_id) _LOGGER.debug( - "Schedule %s started on %s", self.name, self._controller.name, + "Schedule %s started on %s", + self.name, + self._controller.name, ) def turn_off(self, **kwargs) -> None: diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index c5e7f2f956e..4383cf97a2d 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -371,5 +371,6 @@ class RadioThermostat(ClimateEntity): self.device.program_mode = PRESET_MODE_TO_CODE[preset_mode] else: _LOGGER.error( - "preset_mode %s not in PRESET_MODES", preset_mode, + "preset_mode %s not in PRESET_MODES", + preset_mode, ) diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 1fd162c07b7..9041128be60 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -45,7 +45,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = zone_config.get(CONF_FRIENDLY_NAME) devices.append( RainBirdSwitch( - controller, zone, time, name if name else f"Sprinkler {zone}", + controller, + zone, + time, + name if name else f"Sprinkler {zone}", ) ) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index d2930988aee..f93f498987f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -412,7 +412,8 @@ class Recorder(threading.Thread): except Exception as err: # pylint: disable=broad-except # Must catch the exception to prevent the loop from collapsing _LOGGER.error( - "Error in database connectivity during keepalive: %s", err, + "Error in database connectivity during keepalive: %s", + err, ) self._reopen_event_session() diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index b80f4670c36..fee4480e134 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -30,14 +30,16 @@ def purge_old_data(instance, purge_days: int, repack: bool) -> bool: states = execute(query, to_native=True, validate_entity_ids=False) if states: batch_purge_before = min( - batch_purge_before, states[0].last_updated + timedelta(hours=1), + batch_purge_before, + states[0].last_updated + timedelta(hours=1), ) query = session.query(Events).order_by(Events.time_fired.asc()).limit(1) events = execute(query, to_native=True) if events: batch_purge_before = min( - batch_purge_before, events[0].time_fired + timedelta(hours=1), + batch_purge_before, + events[0].time_fired + timedelta(hours=1), ) _LOGGER.debug("Purging states and events before %s", batch_purge_before) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 07516f2c22c..7503d0fe774 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -96,7 +96,9 @@ def execute(qry, to_native=False, validate_entity_ids=True): ) else: _LOGGER.debug( - "querying %d rows took %fs", len(result), elapsed, + "querying %d rows took %fs", + len(result), + elapsed, ) return result diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 04d2078e182..8b0f4364e09 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -161,7 +161,9 @@ async def async_setup(hass, config): hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=data, + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=data, ) ) return True diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 8f7229299ee..21f3e0b74b3 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -62,7 +62,9 @@ DEVICE_TYPE_DEVICE_CLASS = { async def async_setup_entry( - hass, config_entry, async_add_entities, + hass, + config_entry, + async_add_entities, ): """Set up platform.""" sensors = [] diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 8b5886191a5..fc6ab6cbf15 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -21,7 +21,9 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass, config_entry, async_add_entities, + hass, + config_entry, + async_add_entities, ): """Set up config entry.""" discovery_info = config_entry.data diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 44472b6c33c..791cc158693 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -29,7 +29,9 @@ SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS async def async_setup_entry( - hass, config_entry, async_add_entities, + hass, + config_entry, + async_add_entities, ): """Set up config entry.""" discovery_info = config_entry.data diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 8110e8d8c6c..4acde6b0450 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -55,7 +55,9 @@ CONVERT_FUNCTIONS = { async def async_setup_entry( - hass, config_entry, async_add_entities, + hass, + config_entry, + async_add_entities, ): """Set up platform.""" discovery_info = config_entry.data diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 9b2c3c60539..bce5222b778 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -26,7 +26,9 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass, config_entry, async_add_entities, + hass, + config_entry, + async_add_entities, ): """Set up config entry.""" discovery_info = config_entry.data diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 7f097d48a5f..ed22575bccc 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -202,12 +202,15 @@ class GlobalDataUpdater: return except requests.Timeout: _LOGGER.warning( - "Time out fetching Ring %s data", self.data_type, + "Time out fetching Ring %s data", + self.data_type, ) return except requests.RequestException as err: _LOGGER.warning( - "Error fetching Ring %s data: %s", self.data_type, err, + "Error fetching Ring %s data: %s", + self.data_type, + err, ) return diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 96b1a962a67..a313bcf03ba 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -109,7 +109,10 @@ class RingCam(RingEntityMixin, Camera): return image = await asyncio.shield( - ffmpeg.get_image(self._video_url, output_format=IMAGE_JPEG,) + ffmpeg.get_image( + self._video_url, + output_format=IMAGE_JPEG, + ) ) return image diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index fd9dbe0a17e..607e4f1937b 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -19,7 +19,10 @@ async def validate_input(hass: core.HomeAssistant, data): try: token = await hass.async_add_executor_job( - auth.fetch_token, data["username"], data["password"], data.get("2fa"), + auth.fetch_token, + data["username"], + data["password"], + data.get("2fa"), ) except MissingTokenError: raise Require2FA @@ -72,7 +75,8 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user({**self.user_pass, **user_input}) return self.async_show_form( - step_id="2fa", data_schema=vol.Schema({"2fa": str}), + step_id="2fa", + data_schema=vol.Schema({"2fa": str}), ) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 6ee126145b3..bfdf322d4d5 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -93,7 +93,10 @@ class RiscoDataUpdateCoordinator(DataUpdateCoordinator): self.risco = risco interval = timedelta(seconds=scan_interval) super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=interval, + hass, + _LOGGER, + name=DOMAIN, + update_interval=interval, ) async def _async_update_data(self): diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 2627d68e3c3..1baff67f580 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -50,7 +50,9 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: for entry_config in config[DOMAIN]: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=entry_config, ) ) @@ -112,7 +114,10 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): """Class to manage fetching Roku data.""" def __init__( - self, hass: HomeAssistantType, *, host: str, + self, + hass: HomeAssistantType, + *, + host: str, ): """Initialize global Roku data updater.""" self.roku = Roku(host=host, session=async_get_clientsession(hass)) @@ -121,7 +126,10 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): self.last_full_update = None super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, ) async def _async_update_data(self) -> Device: diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 27ab63c728b..662e22605c8 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -56,7 +56,9 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): def _show_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: """Show the form to the user.""" return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors or {}, + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors or {}, ) async def async_step_import( @@ -129,5 +131,6 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): ) return self.async_create_entry( - title=self.discovery_info[CONF_NAME], data=self.discovery_info, + title=self.discovery_info[CONF_NAME], + data=self.discovery_info, ) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 9a46d189486..30cb4ec7a3e 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -58,7 +58,9 @@ async def async_setup_entry(hass, entry, async_add_entities): platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_SEARCH, SEARCH_SCHEMA, "search", + SERVICE_SEARCH, + SEARCH_SCHEMA, + "search", ) diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 5e7d47d2d57..f2daaa0fbf8 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -71,7 +71,9 @@ async def async_setup(hass, config): _LOGGER.debug("Importing Roomba #%d - %s", index, conf[CONF_HOST]) hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf, + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=conf, ) ) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index b939479a45a..9c18600670c 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -77,7 +77,10 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } if self._bridge.token: data[CONF_TOKEN] = self._bridge.token - return self.async_create_entry(title=self._title, data=data,) + return self.async_create_entry( + title=self._title, + data=data, + ) def _try_connect(self): """Try to connect and check auth.""" diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 4b504b5688a..6e406f60ec4 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -77,14 +77,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Initialize bridge data = config_entry.data.copy() bridge = SamsungTVBridge.get_bridge( - data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], data.get(CONF_TOKEN), + data[CONF_METHOD], + data[CONF_HOST], + data[CONF_PORT], + data.get(CONF_TOKEN), ) if bridge.port is None and bridge.default_port is not None: # For backward compat, set default port for websocket tv data[CONF_PORT] = bridge.default_port hass.config_entries.async_update_entry(config_entry, data=data) bridge = SamsungTVBridge.get_bridge( - data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], data.get(CONF_TOKEN), + data[CONF_METHOD], + data[CONF_HOST], + data[CONF_PORT], + data.get(CONF_TOKEN), ) async_add_entities([SamsungTVDevice(bridge, config_entry, on_script)]) @@ -117,7 +123,9 @@ class SamsungTVDevice(MediaPlayerEntity): LOGGER.debug("Access denied in getting remote object") self.hass.add_job( self.hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth"}, data=self._config_entry.data, + DOMAIN, + context={"source": "reauth"}, + data=self._config_entry.data, ) ) diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 8ccb2ad12ff..538267aa355 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -208,7 +208,13 @@ class SenseTrendsSensor(Entity): """Implementation of a Sense energy sensor.""" def __init__( - self, data, name, sensor_type, is_production, trends_coordinator, unique_id, + self, + data, + name, + sensor_type, + is_production, + trends_coordinator, + unique_id, ): """Initialize the Sense sensor.""" name_type = PRODUCTION_NAME if is_production else CONSUMPTION_NAME diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index da57a94e628..830cb81c74e 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -128,5 +128,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get info from shelly device.""" async with async_timeout.timeout(5): return await aioshelly.get_info( - aiohttp_client.async_get_clientsession(self.hass), host, + aiohttp_client.async_get_clientsession(self.hass), + host, ) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index d4a076860a1..ef980c25421 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -49,7 +49,10 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): websession = aiohttp_client.async_get_clientsession(self.hass) return await API.login_via_credentials( - self._username, self._password, client_id=client_id, session=websession, + self._username, + self._password, + client_id=client_id, + session=websession, ) async def _async_login_during_step(self, *, step_id, form_schema): @@ -69,7 +72,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if errors: return self.async_show_form( - step_id=step_id, data_schema=form_schema, errors=errors, + step_id=step_id, + data_schema=form_schema, + errors=errors, ) return await self.async_step_finish( @@ -169,7 +174,8 @@ class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): data_schema=vol.Schema( { vol.Optional( - CONF_CODE, default=self.config_entry.options.get(CONF_CODE), + CONF_CODE, + default=self.config_entry.options.get(CONF_CODE), ): str } ), diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 3820863de93..aed67c5c167 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -79,8 +79,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): smappee = Smappee(api=smappee_api, serialnumber=entry.data[CONF_SERIALNUMBER]) await hass.async_add_executor_job(smappee.load_local_service_location) else: - implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) ) smappee_api = api.ConfigEntrySmappeeApi(hass, entry, implementation) diff --git a/homeassistant/components/smappee/binary_sensor.py b/homeassistant/components/smappee/binary_sensor.py index fbd0c89f4b6..ecc00f12370 100644 --- a/homeassistant/components/smappee/binary_sensor.py +++ b/homeassistant/components/smappee/binary_sensor.py @@ -61,7 +61,9 @@ class SmappeePresence(BinarySensorEntity): return "presence" @property - def unique_id(self,): + def unique_id( + self, + ): """Return the unique ID for this binary sensor.""" return ( f"{self._service_location.device_serial_number}-" @@ -142,7 +144,9 @@ class SmappeeAppliance(BinarySensorEntity): return icon_mapping.get(self._appliance_type) @property - def unique_id(self,): + def unique_id( + self, + ): """Return the unique ID for this binary sensor.""" return ( f"{self._service_location.device_serial_number}-" diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index cffdb7c5024..dd145c4cdce 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -291,7 +291,9 @@ class SmappeeSensor(Entity): return self._unit_of_measurement @property - def unique_id(self,): + def unique_id( + self, + ): """Return the unique ID for this sensor.""" if self._sensor in ["load", "sensor"]: return ( diff --git a/homeassistant/components/smappee/switch.py b/homeassistant/components/smappee/switch.py index 4d158df852a..bbcacc7d541 100644 --- a/homeassistant/components/smappee/switch.py +++ b/homeassistant/components/smappee/switch.py @@ -144,7 +144,9 @@ class SmappeeActuator(SwitchEntity): return None @property - def unique_id(self,): + def unique_id( + self, + ): """Return the unique ID for this switch.""" if self._actuator_type == "INFINITY_OUTPUT_MODULE": return ( diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 82c550e0060..cc6499f9c8d 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -38,7 +38,9 @@ async def async_setup(hass, config) -> bool: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=sh_conf, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=sh_conf, ) ) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 479df05fbb4..48e56e74f70 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -315,7 +315,8 @@ class DeviceBroker: async def regenerate_refresh_token(now): """Generate a new refresh token and update the config entry.""" await self._token.refresh( - self._entry.data[CONF_CLIENT_ID], self._entry.data[CONF_CLIENT_SECRET], + self._entry.data[CONF_CLIENT_ID], + self._entry.data[CONF_CLIENT_SECRET], ) self._hass.config_entries.async_update_entry( self._entry, diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index f8746b597de..e320c335a08 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -80,7 +80,8 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Show the confirmation if user_input is None: return self.async_show_form( - step_id="user", description_placeholders={"webhook_url": webhook_url}, + step_id="user", + description_placeholders={"webhook_url": webhook_url}, ) # Show the next screen diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 0b8f9d986e3..b1af205fc10 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -31,7 +31,9 @@ async def async_setup(hass, config): hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=sms_config, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=sms_config, ) ) diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index 08168994b07..64d2bf9bd98 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -17,7 +17,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] imei = await gateway.get_imei_async() name = f"gsm_signal_imei_{imei}" - entities.append(GSMSignalSensor(hass, gateway, name,)) + entities.append( + GSMSignalSensor( + hass, + gateway, + name, + ) + ) async_add_entities(entities, True) @@ -25,7 +31,10 @@ class GSMSignalSensor(Entity): """Implementation of a GSM Signal sensor.""" def __init__( - self, hass, gateway, name, + self, + hass, + gateway, + name, ): """Initialize the GSM Signal sensor.""" self._hass = hass diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index fb20fcd6683..fbc76e7c938 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -82,8 +82,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): entry, data={**entry.data, "auth_implementation": DOMAIN} ) - implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) ) hass.data[DOMAIN][API] = api.ConfigEntrySomfyApi(hass, entry, implementation) diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py index 761ee19f8cb..a679af06d73 100644 --- a/homeassistant/components/somfy/api.py +++ b/homeassistant/components/somfy/api.py @@ -25,7 +25,9 @@ class ConfigEntrySomfyApi(somfy_api.SomfyApi): ) super().__init__(None, None, token=self.session.token) - def refresh_tokens(self,) -> Dict[str, Union[str, int]]: + def refresh_tokens( + self, + ) -> Dict[str, Union[str, int]]: """Refresh and return new Somfy tokens using Home Assistant OAuth2 session.""" run_coroutine_threadsafe( self.session.async_ensure_token_valid(), self.hass.loop diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index e82ecb49fda..ec1a29c660b 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -111,7 +111,9 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): ] = bool return self.async_show_form( - step_id="user", data_schema=vol.Schema(data_schema), errors=errors or {}, + step_id="user", + data_schema=vol.Schema(data_schema), + errors=errors or {}, ) diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index 4a4332cb0a5..493a3ef4913 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -31,7 +31,9 @@ async def async_setup(hass: HomeAssistantType, config: OrderedDict) -> bool: for config_entry in conf: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config_entry, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config_entry, ), ) return True diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4e4f7338a10..a2440802139 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -450,7 +450,10 @@ class SonosEntity(MediaPlayerEntity): @soco_coordinator def state(self): """Return the state of the entity.""" - if self._status in ("PAUSED_PLAYBACK", "STOPPED",): + if self._status in ( + "PAUSED_PLAYBACK", + "STOPPED", + ): # Sonos can consider itself "paused" but without having media loaded # (happens if playing Spotify and via Spotify app you pick another device to play on) if self.media_title is None: diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 876518f25ed..3d257174328 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -128,7 +128,10 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): self.servers = {} self._unsub_update_listener = None super().__init__( - self.hass, _LOGGER, name=DOMAIN, update_method=self.async_update, + self.hass, + _LOGGER, + name=DOMAIN, + update_method=self.async_update, ) def update_servers(self): diff --git a/homeassistant/components/spider/config_flow.py b/homeassistant/components/spider/config_flow.py index e1026f344b0..c8c31221a50 100644 --- a/homeassistant/components/spider/config_flow.py +++ b/homeassistant/components/spider/config_flow.py @@ -62,7 +62,10 @@ class SpiderConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): result = await self.hass.async_add_executor_job(self._try_connect) if result == RESULT_SUCCESS: - return self.async_create_entry(title=DOMAIN, data=self.data,) + return self.async_create_entry( + title=DOMAIN, + data=self.data, + ) if result != RESULT_AUTH_FAILED: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -71,7 +74,9 @@ class SpiderConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors, + step_id="user", + data_schema=DATA_SCHEMA_USER, + errors=errors, ) async def async_step_import(self, import_data): diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 88fba2f6ccf..2dbad960227 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -224,7 +224,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "async_call_query", ) platform.async_register_entity_service( - SERVICE_SYNC, {vol.Required(ATTR_OTHER_PLAYER): cv.string}, "async_sync", + SERVICE_SYNC, + {vol.Required(ATTR_OTHER_PLAYER): cv.string}, + "async_sync", ) platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index bd7a6966c9d..1e97ac222ec 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -72,7 +72,8 @@ class HlsSegmentView(StreamView): return web.HTTPNotFound() headers = {"Content-Type": "video/iso.segment"} return web.Response( - body=get_m4s(segment.segment, int(sequence)), headers=headers, + body=get_m4s(segment.segment, int(sequence)), + headers=headers, ) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index bed8b25dbd1..ea4359e2a0c 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -199,7 +199,11 @@ def stream_worker(hass, stream, quit_event): if stream.outputs.get(fmt): hass.loop.call_soon_threadsafe( stream.outputs[fmt].put, - Segment(sequence, buffer.segment, segment_duration,), + Segment( + sequence, + buffer.segment, + segment_duration, + ), ) # Reinitialize diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index efd5048053f..0cb96731058 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -203,7 +203,10 @@ class DeviceConnectivity(SurePetcareBinarySensor): """Sure Petcare Pet.""" def __init__( - self, _id: int, sure_type: SureProductID, spc: SurePetcareAPI, + self, + _id: int, + sure_type: SureProductID, + spc: SurePetcareAPI, ) -> None: """Initialize a Sure Petcare Device.""" super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, sure_type) diff --git a/homeassistant/components/syncthru/config_flow.py b/homeassistant/components/syncthru/config_flow.py index ef3b6903358..cbdd46b4a6a 100644 --- a/homeassistant/components/syncthru/config_flow.py +++ b/homeassistant/components/syncthru/config_flow.py @@ -74,7 +74,8 @@ class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_check_and_create("confirm", user_input) return await self._async_show_form( - step_id="confirm", user_input={CONF_URL: self.url, CONF_NAME: self.name}, + step_id="confirm", + user_input={CONF_URL: self.url, CONF_NAME: self.name}, ) async def _async_show_form(self, step_id, user_input=None, errors=None): @@ -133,5 +134,6 @@ class SyncThruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_create_entry( - title=user_input.get(CONF_NAME), data=user_input, + title=user_input.get(CONF_NAME), + data=user_input, ) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 89dc39e427c..2e4b550337b 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -88,7 +88,9 @@ async def async_setup(hass, config): for dsm_conf in conf: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=dsm_conf, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=dsm_conf, ) ) @@ -335,7 +337,10 @@ class SynologyDSMEntity(Entity): """Representation of a Synology NAS entry.""" def __init__( - self, api: SynoApi, entity_type: str, entity_info: Dict[str, str], + self, + api: SynoApi, + entity_type: str, + entity_info: Dict[str, str], ): """Initialize the Synology DSM entity.""" self._api = api diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index f2273bcae10..37e92ff5b4f 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -63,7 +63,9 @@ async def async_setup(hass: HomeAssistant, config: dict): for conf in config[DOMAIN]: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=conf, ) ) @@ -100,7 +102,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Poll for updates in the background update_track = async_track_time_interval( - hass, lambda now: tadoconnector.update(), SCAN_INTERVAL, + hass, + lambda now: tadoconnector.update(), + SCAN_INTERVAL, ) update_listener = entry.add_update_listener(_async_update_listener) @@ -211,7 +215,9 @@ class TadoConnector: return except RuntimeError: _LOGGER.error( - "Unable to connect to Tado while updating %s %s", sensor_type, sensor, + "Unable to connect to Tado while updating %s %s", + sensor_type, + sensor, ) return @@ -239,7 +245,8 @@ class TadoConnector: self.update_sensor("zone", zone_id) def set_presence( - self, presence=PRESET_HOME, + self, + presence=PRESET_HOME, ): """Set the presence to home or away.""" if presence == PRESET_AWAY: diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 6d45552abd2..3e0c79ad65e 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -75,7 +75,9 @@ async def async_setup_entry( platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_CLIMATE_TIMER, CLIMATE_TIMER_SCHEMA, "set_timer", + SERVICE_CLIMATE_TIMER, + CLIMATE_TIMER_SCHEMA, + "set_timer", ) if entities: diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index be3be0f545a..1a99db5c24c 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -71,7 +71,9 @@ async def async_setup_entry( platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_WATER_HEATER_TIMER, WATER_HEATER_TIMER_SCHEMA, "set_timer", + SERVICE_WATER_HEATER_TIMER, + WATER_HEATER_TIMER_SCHEMA, + "set_timer", ) if entities: diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index e26c0fca8f4..1f9988bcafa 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -291,7 +291,8 @@ class CoverTemplate(TemplateEntity, CoverEntity): if state < 0 or state > 100: self._tilt_value = None _LOGGER.error( - "Tilt value must be between 0 and 100. Value was: %.2f", state, + "Tilt value must be between 0 and 100. Value was: %.2f", + state, ) else: self._tilt_value = state diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 01cf22c4aab..8017bef163b 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -385,7 +385,8 @@ class TemplateFan(TemplateEntity, FanEntity): self._oscillating = None else: _LOGGER.error( - "Received invalid oscillating: %s. Expected: True/False", oscillating, + "Received invalid oscillating: %s. Expected: True/False", + oscillating, ) self._oscillating = None diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 6375995ed7d..615995758a1 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -341,7 +341,10 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): ) if self._fan_speed_template is not None: self.add_template_attribute( - "_fan_speed", self._fan_speed_template, None, self._update_fan_speed, + "_fan_speed", + self._fan_speed_template, + None, + self._update_fan_speed, ) if self._battery_level_template is not None: self.add_template_attribute( diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 420c3403a11..d293a19d8d6 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -204,7 +204,12 @@ class TensorFlowImageProcessor(ImageProcessingEntity): """Representation of an TensorFlow image processor.""" def __init__( - self, hass, camera_entity, name, category_index, config, + self, + hass, + camera_entity, + name, + category_index, + config, ): """Initialize the TensorFlow entity.""" model_config = config.get(CONF_MODEL) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index fefe980fbd1..bc2a09788e5 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -223,7 +223,10 @@ class TeslaDataUpdateCoordinator(DataUpdateCoordinator): update_interval = timedelta(seconds=MIN_SCAN_INTERVAL) super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=update_interval, + hass, + _LOGGER, + name=DOMAIN, + update_interval=update_interval, ) async def _async_update_data(self): diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 01dc1aa44f0..3c3777afc1f 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -13,7 +13,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities( [ TeslaBinarySensor( - device, hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], ) for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ "binary_sensor" diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index bfc2f721a4b..4c7ed850749 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -25,7 +25,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities( [ TeslaThermostat( - device, hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], ) for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ "climate" diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index c3c82a708bf..46265a96ae4 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -14,7 +14,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" entities = [ TeslaDeviceEntity( - device, hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], ) for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ "devices_tracker" diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 9f6db402422..7a74d2ececb 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -12,7 +12,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" entities = [ TeslaLock( - device, hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["coordinator"], ) for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["lock"] ] diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py index b0115d84e2c..2e09ea22777 100644 --- a/homeassistant/components/tibber/config_flow.py +++ b/homeassistant/components/tibber/config_flow.py @@ -54,7 +54,9 @@ class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if errors: return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors, + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, ) unique_id = tibber_connection.user_id @@ -62,7 +64,12 @@ class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( - title=tibber_connection.name, data={CONF_ACCESS_TOKEN: access_token}, + title=tibber_connection.name, + data={CONF_ACCESS_TOKEN: access_token}, ) - return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors={},) + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={}, + ) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 25b1141bd9b..cf3f059cfb9 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -36,7 +36,9 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN], + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], ) ) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 9c3d73b43b5..a5135704dad 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -191,14 +191,18 @@ class TPLinkSmartBulb(LightEntity): await self._async_set_light_state_retry( self._light_state, self._light_state._replace( - state=True, brightness=brightness, color_temp=color_tmp, hs=hue_sat, + state=True, + brightness=brightness, + color_temp=color_tmp, + hs=hue_sat, ), ) async def async_turn_off(self, **kwargs): """Turn the light off.""" await self._async_set_light_state_retry( - self._light_state, self._light_state._replace(state=False), + self._light_state, + self._light_state._replace(state=False), ) @property diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index 8b43850623a..56ed3081b63 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -80,7 +80,9 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors, + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, ) async def async_step_import(self, import_config): diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index baa70bf0b19..69388de1145 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -153,7 +153,9 @@ class TransmissionTorrentsSensor(TransmissionSensor): order = self._tm_client.config_entry.options[CONF_ORDER] torrents = self._tm_client.api.torrents[0:limit] info = _torrents_info( - torrents, order=order, statuses=self.SUBTYPE_MODES[self._sub_type], + torrents, + order=order, + statuses=self.SUBTYPE_MODES[self._sub_type], ) return { STATE_ATTR_TORRENT_INFO: info, diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index e2304fc7f63..6524c026fcf 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -100,7 +100,8 @@ async def async_setup_entry(hass, entry): except TuyaAPIException as exc: _LOGGER.error( - "Connection error during integration setup. Error: %s", exc, + "Connection error during integration setup. Error: %s", + exc, ) return False diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 15cecefdb21..99939c4b9c0 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -54,7 +54,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not dev_ids: return entities = await hass.async_add_executor_job( - _setup_entities, hass, dev_ids, platform, + _setup_entities, + hass, + dev_ids, + platform, ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 538b819cb05..3c94ed6a53d 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -26,7 +26,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not dev_ids: return entities = await hass.async_add_executor_job( - _setup_entities, hass, dev_ids, platform, + _setup_entities, + hass, + dev_ids, + platform, ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 6144bd4ab96..cc8272fabba 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -25,7 +25,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not dev_ids: return entities = await hass.async_add_executor_job( - _setup_entities, hass, dev_ids, platform, + _setup_entities, + hass, + dev_ids, + platform, ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 9416089f898..ee9c92221df 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -30,7 +30,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not dev_ids: return entities = await hass.async_add_executor_job( - _setup_entities, hass, dev_ids, platform, + _setup_entities, + hass, + dev_ids, + platform, ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index 39613318379..40f9fca11ee 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -23,7 +23,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not dev_ids: return entities = await hass.async_add_executor_job( - _setup_entities, hass, dev_ids, platform, + _setup_entities, + hass, + dev_ids, + platform, ) async_add_entities(entities) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 13b4af94a48..e4d0778cab0 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -23,7 +23,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not dev_ids: return entities = await hass.async_add_executor_job( - _setup_entities, hass, dev_ids, platform, + _setup_entities, + hass, + dev_ids, + platform, ) async_add_entities(entities) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 82314adb771..3a3229415ab 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -323,7 +323,9 @@ class UniFiController: client = self.api.clients_all[mac] self.api.clients.process_raw([client.raw]) LOGGER.debug( - "Restore disconnected client %s (%s)", entity.entity_id, client.mac, + "Restore disconnected client %s (%s)", + entity.entity_id, + client.mac, ) wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 3a34cb26604..8479bd06518 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -122,7 +122,8 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) # Ensure entry has proper unique_id. if config_entry.unique_id != device.unique_id: hass.config_entries.async_update_entry( - entry=config_entry, unique_id=device.unique_id, + entry=config_entry, + unique_id=device.unique_id, ) # Create device registry entry. diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 0c57a2c243e..016a5a25017 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -90,7 +90,10 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), } ) - return self.async_show_form(step_id="user", data_schema=data_schema,) + return self.async_show_form( + step_id="user", + data_schema=data_schema, + ) async def async_step_import(self, import_info: Optional[Mapping]): """Import a new UPnP/IGD device as a config entry. @@ -183,11 +186,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return UpnpOptionsFlowHandler(config_entry) async def _async_create_entry_from_discovery( - self, discovery: Mapping, + self, + discovery: Mapping, ): """Create an entry from discovery.""" _LOGGER.debug( - "_async_create_entry_from_data: discovery: %s", discovery, + "_async_create_entry_from_data: discovery: %s", + discovery, ) # Get name from device, if not found already. if DISCOVERY_NAME not in discovery and DISCOVERY_LOCATION in discovery: @@ -238,9 +243,10 @@ class UpnpOptionsFlowHandler(config_entries.OptionsFlow): step_id="init", data_schema=vol.Schema( { - vol.Optional(CONF_SCAN_INTERVAL, default=scan_interval,): vol.All( - vol.Coerce(int), vol.Range(min=30) - ), + vol.Optional( + CONF_SCAN_INTERVAL, + default=scan_interval, + ): vol.All(vol.Coerce(int), vol.Range(min=30)), } ), ) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 263f5f0025b..b45716b33d6 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -61,7 +61,9 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config, + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=config, ) ) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index cac17951cc1..a040e4b96b5 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -42,10 +42,12 @@ def options_schema(options: dict = None) -> dict: options = options or {} return { vol.Optional( - CONF_LIGHTS, default=list_to_str(options.get(CONF_LIGHTS, [])), + CONF_LIGHTS, + default=list_to_str(options.get(CONF_LIGHTS, [])), ): str, vol.Optional( - CONF_EXCLUDE, default=list_to_str(options.get(CONF_EXCLUDE, [])), + CONF_EXCLUDE, + default=list_to_str(options.get(CONF_EXCLUDE, [])), ): str, } @@ -68,7 +70,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=options_data(user_input),) + return self.async_create_entry( + title="", + data=options_data(user_input), + ) return self.async_show_form( step_id="init", diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index a2f67b1d5c8..cbddbaa6897 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -226,8 +226,10 @@ class VizioDevice(MediaPlayerEntity): self._supported_commands |= SUPPORT_SELECT_SOUND_MODE self._current_sound_mode = audio_settings[VIZIO_SOUND_MODE] if not self._available_sound_modes: - self._available_sound_modes = await self._device.get_setting_options( - VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE + self._available_sound_modes = ( + await self._device.get_setting_options( + VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE + ) ) else: # Explicitly remove SUPPORT_SELECT_SOUND_MODE from supported features @@ -291,7 +293,9 @@ class VizioDevice(MediaPlayerEntity): ) -> None: """Update a setting when update_setting service is called.""" await self._device.set_setting( - setting_type, setting_name, new_value, + setting_type, + setting_name, + new_value, ) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index cdac719d890..90e55ea08a0 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -74,11 +74,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.debug("The following stations were returned: %s", stations) for station in stations: waqi_sensor = WaqiSensor(client, station) - if not station_filter or { - waqi_sensor.uid, - waqi_sensor.url, - waqi_sensor.station_name, - } & set(station_filter): + if ( + not station_filter + or { + waqi_sensor.uid, + waqi_sensor.url, + waqi_sensor.station_name, + } + & set(station_filter) + ): dev.append(waqi_sensor) except (aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError): _LOGGER.exception("Failed to connect to WAQI servers") diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 9cac85dee09..0594747d0b2 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -121,7 +121,8 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("Scanning network for WeMo devices...") for device in await hass.async_add_executor_job(pywemo.discover_devices): devices.setdefault( - device.serialnumber, device, + device.serialnumber, + device, ) loaded_components = set() @@ -153,7 +154,9 @@ async def async_setup_entry(hass, entry): else: async_dispatcher_send( - hass, f"{DOMAIN}.{component}", device, + hass, + f"{DOMAIN}.{component}", + device, ) return True @@ -172,7 +175,10 @@ def validate_static_config(host, port): try: device = pywemo.discovery.device_from_description(url, None) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,) as err: + except ( + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + ) as err: _LOGGER.error("Unable to access WeMo at %s (%s)", url, err) return None diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index f2cb46fa32c..1bc477277c9 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -129,7 +129,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Register service(s) hass.services.async_register( - WEMO_DOMAIN, SERVICE_SET_HUMIDITY, service_handle, schema=SET_HUMIDITY_SCHEMA, + WEMO_DOMAIN, + SERVICE_SET_HUMIDITY, + service_handle, + schema=SET_HUMIDITY_SCHEMA, ) hass.services.async_register( diff --git a/homeassistant/components/wilight/parent_device.py b/homeassistant/components/wilight/parent_device.py index 9c2c0c1a1de..a53bc352a7b 100644 --- a/homeassistant/components/wilight/parent_device.py +++ b/homeassistant/components/wilight/parent_device.py @@ -95,7 +95,10 @@ def create_api_device(host): """Create an API Device.""" try: device = pywilight.device_from_host(host) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,) as err: + except ( + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + ) as err: _LOGGER.error("Unable to access WiLight at %s (%s)", host, err) return None diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 2c2add94527..0b0a6ee519f 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -122,7 +122,8 @@ class WirelessTagPlatform: ) else: _LOGGER.info( - "Installed push notifications for all tags in %s", mac, + "Installed push notifications for all tags in %s", + mac, ) @property diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 2f3f4bf849a..23c68facb6f 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -53,7 +53,10 @@ from . import const from .const import Measurement _LOGGER = logging.getLogger(const.LOG_NAMESPACE) -NOT_AUTHENTICATED_ERROR = re.compile("^401,.*", re.IGNORECASE,) +NOT_AUTHENTICATED_ERROR = re.compile( + "^401,.*", + re.IGNORECASE, +) DATA_UPDATED_SIGNAL = "withings_entity_state_updated" MeasurementData = Dict[Measurement, Any] @@ -621,8 +624,8 @@ class DataManager: def empty_listener() -> None: pass - self._cancel_subscription_update = self.subscription_update_coordinator.async_add_listener( - empty_listener + self._cancel_subscription_update = ( + self.subscription_update_coordinator.async_add_listener(empty_listener) ) def async_stop_polling_webhook_subscriptions(self) -> None: @@ -753,7 +756,8 @@ class DataManager: # Start a reauth flow. await self._hass.config_entries.flow.async_init( - const.DOMAIN, context=context, + const.DOMAIN, + context=context, ) return @@ -883,7 +887,9 @@ async def async_get_entity_id( hass: HomeAssistant, attribute: WithingsAttribute, user_id: int ) -> Optional[str]: """Get an entity id for a user's attribute.""" - entity_registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() + entity_registry: EntityRegistry = ( + await hass.helpers.entity_registry.async_get_registry() + ) unique_id = get_attribute_unique_id(attribute, user_id) entity_id = entity_registry.async_get_entity_id( diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index a7580faa3d0..a55d83d717b 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -17,7 +17,10 @@ async def async_setup_entry( """Set up the sensor config entry.""" entities = await async_create_entities( - hass, entry, WithingsHealthSensor, SENSOR_DOMAIN, + hass, + entry, + WithingsHealthSensor, + SENSOR_DOMAIN, ) async_add_entities(entities, True) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 5cc2453d78c..76b61dd7808 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -111,13 +111,19 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): """Class to manage fetching WLED data from single endpoint.""" def __init__( - self, hass: HomeAssistant, *, host: str, + self, + hass: HomeAssistant, + *, + host: str, ): """Initialize global WLED data updater.""" self.wled = WLED(host, session=async_get_clientsession(hass)) super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, ) def update_listeners(self) -> None: diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 86daa6e994a..3a3853d9f68 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -32,7 +32,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= time_zone = dt_util.get_time_zone(config.get(CONF_TIME_ZONE)) async_add_entities( - [WorldClockSensor(time_zone, name, config.get(CONF_TIME_FORMAT),)], True, + [ + WorldClockSensor( + time_zone, + name, + config.get(CONF_TIME_FORMAT), + ) + ], + True, ) diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index 0709c7e83fa..fecdfd91a75 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -295,7 +295,12 @@ class XiaomiDoorSensor(XiaomiBinarySensor): else: data_key = "window_status" super().__init__( - device, "Door Window Sensor", xiaomi_hub, data_key, "opening", config_entry, + device, + "Door Window Sensor", + xiaomi_hub, + data_key, + "opening", + config_entry, ) @property @@ -340,7 +345,12 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor): else: data_key = "wleak_status" super().__init__( - device, "Water Leak Sensor", xiaomi_hub, data_key, "moisture", config_entry, + device, + "Water Leak Sensor", + xiaomi_hub, + data_key, + "moisture", + config_entry, ) def parse_data(self, data, raw_data): diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index eb4e6df9acd..9c8d06e0b0d 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -158,10 +158,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_service_learn_handler, ) platform.async_register_entity_service( - SERVICE_SET_REMOTE_LED_ON, {}, async_service_led_on_handler, + SERVICE_SET_REMOTE_LED_ON, + {}, + async_service_led_on_handler, ) platform.async_register_entity_service( - SERVICE_SET_REMOTE_LED_OFF, {}, async_service_led_off_handler, + SERVICE_SET_REMOTE_LED_OFF, + {}, + async_service_led_off_handler, ) diff --git a/homeassistant/components/xs1/__init__.py b/homeassistant/components/xs1/__init__.py index 1fbcb49d0c9..8651c33546c 100644 --- a/homeassistant/components/xs1/__init__.py +++ b/homeassistant/components/xs1/__init__.py @@ -63,7 +63,8 @@ def setup(hass, config): ) except ConnectionError as error: _LOGGER.error( - "Failed to create XS1 API client because of a connection error: %s", error, + "Failed to create XS1 API client because of a connection error: %s", + error, ) return False diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 196e6605eab..5147e57dfc6 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -154,7 +154,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Register Service 'select_scene' platform = entity_platform.current_platform.get() platform.async_register_entity_service( - SERVICE_SELECT_SCENE, {vol.Required(ATTR_SCENE): cv.string}, "set_scene", + SERVICE_SELECT_SCENE, + {vol.Required(ATTR_SCENE): cv.string}, + "set_scene", ) # Register Service 'enable_output' platform.async_register_entity_service( diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index e3a2044454a..473d39c6f7a 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -60,7 +60,10 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if auto_detected_data is not None: title = f"{port.description}, s/n: {port.serial_number or 'n/a'}" title += f" - {port.manufacturer}" if port.manufacturer else "" - return self.async_create_entry(title=title, data=auto_detected_data,) + return self.async_create_entry( + title=title, + data=auto_detected_data, + ) # did not detect anything self._device_path = dev_path @@ -78,7 +81,8 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): schema = {vol.Required(CONF_RADIO_TYPE): vol.In(sorted(RadioType.list()))} return self.async_show_form( - step_id="pick_radio", data_schema=vol.Schema(schema), + step_id="pick_radio", + data_schema=vol.Schema(schema), ) async def async_step_port_config(self, user_input=None): @@ -113,7 +117,9 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): schema[param] = value return self.async_show_form( - step_id="port_config", data_schema=vol.Schema(schema), errors=errors, + step_id="port_config", + data_schema=vol.Schema(schema), + errors=errors, ) diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 56345916cd3..17fbb86fc63 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -332,7 +332,10 @@ class ThermostatChannel(ZigbeeChannel): if not isinstance(res, list): # assume default response self.debug( - "attr reporting for '%s' on '%s': %s", attrs, self.name, res, + "attr reporting for '%s' on '%s': %s", + attrs, + self.name, + res, ) return if res[0].status == Status.SUCCESS and len(res) == 1: diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 5f65f1e9596..c3e1beb44af 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -76,7 +76,8 @@ def empty_value(value: Any) -> Any: CONFIG_SCHEMA = vol.Schema( { vol.Optional(DOMAIN, default=[]): vol.Any( - vol.All(cv.ensure_list, [vol.Schema(CREATE_FIELDS)]), empty_value, + vol.All(cv.ensure_list, [vol.Schema(CREATE_FIELDS)]), + empty_value, ) }, extra=vol.ALLOW_EXTRA, @@ -234,7 +235,10 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: if component.get_entity("zone.home"): return True - home_zone = Zone(_home_conf(hass), True,) + home_zone = Zone( + _home_conf(hass), + True, + ) home_zone.entity_id = ENTITY_ID_HOME await component.async_add_entities([home_zone]) diff --git a/homeassistant/config.py b/homeassistant/config.py index a327ca630f8..80b5a203564 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -809,9 +809,7 @@ async def async_process_component_config( # Validate platform specific schema if hasattr(platform, "PLATFORM_SCHEMA"): try: - p_validated = platform.PLATFORM_SCHEMA( # type: ignore - p_config - ) + p_validated = platform.PLATFORM_SCHEMA(p_config) # type: ignore except vol.Invalid as ex: async_log_exception( ex, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 01eb63fe05f..878d162c2ff 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -225,9 +225,7 @@ class ConfigEntry: return try: - result = await component.async_setup_entry( # type: ignore - hass, self - ) + result = await component.async_setup_entry(hass, self) # type: ignore if not isinstance(result, bool): _LOGGER.error( @@ -312,9 +310,7 @@ class ConfigEntry: return False try: - result = await component.async_unload_entry( # type: ignore - hass, self - ) + result = await component.async_unload_entry(hass, self) # type: ignore assert isinstance(result, bool) @@ -349,9 +345,7 @@ class ConfigEntry: if not hasattr(component, "async_remove_entry"): return try: - await component.async_remove_entry( # type: ignore - hass, self - ) + await component.async_remove_entry(hass, self) # type: ignore except Exception: # pylint: disable=broad-except _LOGGER.exception( "Error calling entry remove callback %s for %s", @@ -389,9 +383,7 @@ class ConfigEntry: return False try: - result = await component.async_migrate_entry( # type: ignore - hass, self - ) + result = await component.async_migrate_entry(hass, self) # type: ignore if not isinstance(result, bool): _LOGGER.error( "%s.async_migrate_entry did not return boolean", self.domain @@ -878,7 +870,9 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def _abort_if_unique_id_configured( - self, updates: Optional[Dict[Any, Any]] = None, reload_on_update: bool = True, + self, + updates: Optional[Dict[Any, Any]] = None, + reload_on_update: bool = True, ) -> None: """Abort if the unique ID is already configured.""" assert self.hass diff --git a/homeassistant/core.py b/homeassistant/core.py index c5a54f0374f..6b44386eadf 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -319,9 +319,7 @@ class HomeAssistant: elif is_callback(check_target): self.loop.call_soon(target, *args) else: - task = self.loop.run_in_executor( # type: ignore - None, target, *args - ) + task = self.loop.run_in_executor(None, target, *args) # type: ignore # If a task is scheduled if self._track_task and task is not None: diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 49195e1a89a..b553c190f55 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -51,7 +51,10 @@ class AbortFlow(FlowError): class FlowManager(abc.ABC): """Manage all the flows that are in progress.""" - def __init__(self, hass: HomeAssistant,) -> None: + def __init__( + self, + hass: HomeAssistant, + ) -> None: """Initialize the flow manager.""" self.hass = hass self._initializing: Dict[str, List[asyncio.Future]] = {} diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 316c02a82f0..d20e34cca36 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -68,7 +68,9 @@ def async_create_clientsession( connector = _async_get_connector(hass, verify_ssl) clientsession = aiohttp.ClientSession( - connector=connector, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs, + connector=connector, + headers={USER_AGENT: SERVER_SOFTWARE}, + **kwargs, ) clientsession.close = warn_use( # type: ignore diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 72cc4650978..5b3366d7554 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -468,9 +468,7 @@ class Entity(ABC): if hasattr(self, "async_update"): await self.async_update() # type: ignore elif hasattr(self, "update"): - await self.hass.async_add_executor_job( - self.update # type: ignore - ) + await self.hass.async_add_executor_job(self.update) # type: ignore finally: self._update_staged = False if warning: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index e3dcbbc3c79..b9d073dbe2e 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -106,11 +106,7 @@ class EntityComponent: This doesn't block the executor to protect from deadlocks. """ - self.hass.add_job( - self.async_setup( # type: ignore - config - ) - ) + self.hass.add_job(self.async_setup(config)) # type: ignore async def async_setup(self, config: ConfigType) -> None: """Set up a full entity component. diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index d5fad7fd025..32ef88d361f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -322,7 +322,9 @@ class EntityPlatform: return self._async_unsub_polling = async_track_time_interval( - self.hass, self._update_entity_states, self.scan_interval, + self.hass, + self._update_entity_states, + self.scan_interval, ) async def _async_add_entity( diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py index 1df039da47a..5feca605099 100644 --- a/homeassistant/helpers/instance_id.py +++ b/homeassistant/helpers/instance_id.py @@ -18,7 +18,9 @@ async def async_get(hass: HomeAssistant) -> str: store = storage.Store(hass, DATA_VERSION, DATA_KEY, True) data: Optional[Dict[str, str]] = await storage.async_migrator( # type: ignore - hass, hass.config.path(LEGACY_UUID_FILE), store, + hass, + hass.config.path(LEGACY_UUID_FILE), + store, ) if data is not None: diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 73d78501578..4ff9198f233 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -97,5 +97,6 @@ def setup_reload_service( """Sync version of async_setup_reload_service.""" asyncio.run_coroutine_threadsafe( - async_setup_reload_service(hass, domain, platforms), hass.loop, + async_setup_reload_service(hass, domain, platforms), + hass.loop, ).result() diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index c59d53f2e87..7148f633dfb 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -450,7 +450,9 @@ class _ScriptRun: ) except exceptions.TemplateError as ex: self._log( - "Error rendering event data template: %s", ex, level=logging.ERROR, + "Error rendering event data template: %s", + ex, + level=logging.ERROR, ) self._hass.bus.async_fire( diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 13525b4dab1..daf9e2f6a89 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -20,7 +20,12 @@ _LOGGER = logging.getLogger(__name__) @bind_hass async def async_migrator( - hass, old_path, store, *, old_conf_load_func=None, old_conf_migrate_func=None, + hass, + old_path, + store, + *, + old_conf_load_func=None, + old_conf_migrate_func=None, ): """Migrate old data to a store and then load data. diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 217f16d1841..53d808a5a85 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -92,7 +92,9 @@ def load_translations_files( def merge_resources( - translation_strings: Dict[str, Dict[str, Any]], components: Set[str], category: str, + translation_strings: Dict[str, Dict[str, Any]], + components: Set[str], + category: str, ) -> Dict[str, Dict[str, Any]]: """Build and merge the resources response for the given components and platforms.""" # Build response @@ -153,7 +155,9 @@ def merge_resources( def build_resources( - translation_strings: Dict[str, Dict[str, Any]], components: Set[str], category: str, + translation_strings: Dict[str, Dict[str, Any]], + components: Set[str], + category: str, ) -> Dict[str, Dict[str, Any]]: """Build the resources response for the given components.""" # Build response diff --git a/homeassistant/runner.py b/homeassistant/runner.py index b397f9438f2..f35de991a27 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -117,7 +117,9 @@ def _async_loop_exception_handler(_: Any, context: Dict) -> None: ) -async def setup_and_run_hass(runtime_config: RuntimeConfig,) -> int: +async def setup_and_run_hass( + runtime_config: RuntimeConfig, +) -> int: """Set up Home Assistant and run.""" hass = await bootstrap.async_setup_hass(runtime_config) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 341229a83b1..870b476d605 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -177,9 +177,7 @@ async def _async_setup_component( try: if hasattr(component, "async_setup"): - task = component.async_setup( # type: ignore - hass, processed_config - ) + task = component.async_setup(hass, processed_config) # type: ignore elif hasattr(component, "setup"): # This should not be replaced with hass.async_add_executor_job because # we don't want to track this task in case it blocks startup. diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index 908d36a41bb..1193c701712 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -255,7 +255,10 @@ class _ZoneTaskContext: """Context manager that tracks an active task for a zone.""" def __init__( - self, zone: _ZoneTimeoutManager, task: asyncio.Task[Any], timeout: float, + self, + zone: _ZoneTimeoutManager, + task: asyncio.Task[Any], + timeout: float, ) -> None: """Initialize internal timeout context manager.""" self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 8b276da432d..bac66cd28bc 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -137,9 +137,7 @@ class UnitSystem: raise TypeError(f"{volume!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return volume_util.convert( # type: ignore - volume, from_unit, self.volume_unit - ) + return volume_util.convert(volume, from_unit, self.volume_unit) # type: ignore def as_dict(self) -> dict: """Convert the unit system to a dictionary.""" diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 982446597e5..a58480e26b7 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -88,9 +88,7 @@ def _add_reference( ... -def _add_reference( # type: ignore - obj, loader: SafeLineLoader, node: yaml.nodes.Node -): +def _add_reference(obj, loader: SafeLineLoader, node: yaml.nodes.Node): # type: ignore """Add file reference information to an object.""" if isinstance(obj, list): obj = NodeListClass(obj) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 62c8fa113de..ceca5145010 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.6.2 -black==19.10b0 +black==20.8b1 codespell==1.17.1 flake8-docstrings==1.5.0 flake8==3.8.3 diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index d6dee8d0bd0..7cd09ad1f42 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -21,7 +21,8 @@ async def test_form(hass): ), patch( "homeassistant.components.NEW_DOMAIN.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.NEW_DOMAIN.async_setup_entry", return_value=True, + "homeassistant.components.NEW_DOMAIN.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index d561f284caf..20b5a03206e 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -56,8 +56,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up NEW_NAME from a config entry.""" - implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) ) session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) diff --git a/script/translations/clean.py b/script/translations/clean.py index 0eabf6214ae..370c1d672ea 100644 --- a/script/translations/clean.py +++ b/script/translations/clean.py @@ -12,7 +12,10 @@ def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" parser = get_base_arg_parser() parser.add_argument( - "--target", type=str, default="core", choices=["core", "frontend"], + "--target", + type=str, + default="core", + choices=["core", "frontend"], ) return parser.parse_args() diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index d64b211f304..b45599c408e 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -61,7 +61,8 @@ async def test_set_alarm_away(hass): mock_set_away.assert_called_once() with patch( - "abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock, + "abodepy.ALARM.AbodeAlarm.mode", + new_callable=PropertyMock, ) as mock_mode: mock_mode.return_value = CONST.MODE_AWAY diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index 6b7430e524d..2d4769204ac 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -55,7 +55,9 @@ async def test_invalid_api_key(hass): ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG, + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, ) assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -69,7 +71,9 @@ async def test_api_error(hass): ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG, + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, ) assert result["errors"] == {"base": "cannot_connect"} @@ -85,7 +89,9 @@ async def test_requests_exceeded_error(hass): ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG, + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, ) assert result["errors"] == {CONF_API_KEY: "requests_exceeded"} @@ -98,11 +104,15 @@ async def test_integration_already_exists(hass): return_value=json.loads(load_fixture("accuweather/location_data.json")), ): MockConfigEntry( - domain=DOMAIN, unique_id="123456", data=VALID_CONFIG, + domain=DOMAIN, + unique_id="123456", + data=VALID_CONFIG, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG, + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, ) assert result["type"] == "abort" @@ -119,7 +129,9 @@ async def test_create_entry(hass): ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG, + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -133,7 +145,9 @@ async def test_create_entry(hass): async def test_options_flow(hass): """Test config flow options.""" config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="123456", data=VALID_CONFIG, + domain=DOMAIN, + unique_id="123456", + data=VALID_CONFIG, ) config_entry.add_to_hass(hass) diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 69e1285889b..dae9e0da79d 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -156,16 +156,22 @@ async def test_hassio_update_instance_running(hass, aioclient_mock): entry.add_to_hass(hass) with patch.object( - hass.config_entries, "async_forward_entry_setup", return_value=True, + hass.config_entries, + "async_forward_entry_setup", + return_value=True, ) as mock_load: assert await hass.config_entries.async_setup(entry.entry_id) assert entry.state == config_entries.ENTRY_STATE_LOADED assert len(mock_load.mock_calls) == 2 with patch.object( - hass.config_entries, "async_forward_entry_unload", return_value=True, + hass.config_entries, + "async_forward_entry_unload", + return_value=True, ) as mock_unload, patch.object( - hass.config_entries, "async_forward_entry_setup", return_value=True, + hass.config_entries, + "async_forward_entry_setup", + return_value=True, ) as mock_load: result = await hass.config_entries.flow.async_init( "adguard", diff --git a/tests/components/agent_dvr/__init__.py b/tests/components/agent_dvr/__init__.py index f0c059d12e2..2cda0b8aa73 100644 --- a/tests/components/agent_dvr/__init__.py +++ b/tests/components/agent_dvr/__init__.py @@ -9,7 +9,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def init_integration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + skip_setup: bool = False, ) -> MockConfigEntry: """Set up the Agent DVR integration in Home Assistant.""" diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py index 33b5805700f..403dd9f4bee 100644 --- a/tests/components/agent_dvr/test_config_flow.py +++ b/tests/components/agent_dvr/test_config_flow.py @@ -15,7 +15,8 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + config_flow.DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" @@ -70,7 +71,8 @@ async def test_full_user_flow_implementation( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + config_flow.DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 6741731e0e5..8912b0287d7 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -68,7 +68,8 @@ async def test_invalid_identifier(hass): } with patch( - "pyairvisual.api.API.nearest_city", side_effect=InvalidKeyError, + "pyairvisual.api.API.nearest_city", + side_effect=InvalidKeyError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=geography_conf @@ -82,7 +83,8 @@ async def test_node_pro_error(hass): node_pro_conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "my_password"} with patch( - "pyairvisual.node.Node.from_samba", side_effect=NodeProError, + "pyairvisual.node.Node.from_samba", + side_effect=NodeProError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={"type": "AirVisual Node/Pro"} diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1ac01c3d3ff..c3bbcbb7a31 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -3296,7 +3296,10 @@ async def test_media_player_sound_mode_list_unsupported(hass): # Test equalizer controller is not there assert_endpoint_capabilities( - appliance, "Alexa", "Alexa.PowerController", "Alexa.EndpointHealth", + appliance, + "Alexa", + "Alexa.PowerController", + "Alexa.EndpointHealth", ) @@ -3832,7 +3835,8 @@ async def test_camera_hass_urls(hass, mock_stream, url, result): {"friendly_name": "Test camera", "supported_features": 3}, ) await async_process_ha_core_config( - hass, {"external_url": url}, + hass, + {"external_url": url}, ) appliance = await discovery_test(device, hass) @@ -3846,7 +3850,8 @@ async def test_initialize_camera_stream(hass, mock_camera, mock_stream): ) await async_process_ha_core_config( - hass, {"external_url": "https://mycamerastream.test"}, + hass, + {"external_url": "https://mycamerastream.test"}, ) with patch( diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index 3cbcb2d58bb..49694d84410 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -18,7 +18,9 @@ async def test_import(hass): """Test that we can import a config entry.""" with patch("pyalmond.WebAlmondAPI.async_list_apps"): assert await setup.async_setup_component( - hass, DOMAIN, {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, + hass, + DOMAIN, + {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, ) await hass.async_block_till_done() @@ -34,7 +36,9 @@ async def test_import_cannot_connect(hass): "pyalmond.WebAlmondAPI.async_list_apps", side_effect=asyncio.TimeoutError ): assert await setup.async_setup_component( - hass, DOMAIN, {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, + hass, + DOMAIN, + {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, ) await hass.async_block_till_done() diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py index 9fb228dbf66..35a641bac01 100644 --- a/tests/components/almond/test_init.py +++ b/tests/components/almond/test_init.py @@ -99,7 +99,8 @@ async def test_set_up_local(hass, aioclient_mock): # Set up an internal URL, as Almond won't be set up if there is no URL available await async_process_ha_core_config( - hass, {"internal_url": "https://192.168.0.1"}, + hass, + {"internal_url": "https://192.168.0.1"}, ) entry = MockConfigEntry( diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index bf16957b07f..2641c2b349d 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1085,7 +1085,10 @@ async def _test_service( f"androidtv.{androidtv_patch}.{androidtv_method}", return_value=return_value ) as service_call: await hass.services.async_call( - DOMAIN, ha_service_name, service_data=service_data, blocking=True, + DOMAIN, + ha_service_name, + service_data=service_data, + blocking=True, ) assert service_call.called diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 9475c2f110c..032f57c3113 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -56,7 +56,9 @@ def dummy_client_fixture(hass): async def test_ssdp(hass, dummy_client): """Test a ssdp import flow.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=MOCK_DISCOVER, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "confirm" @@ -75,7 +77,9 @@ async def test_ssdp_abort(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=MOCK_DISCOVER, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -86,7 +90,9 @@ async def test_ssdp_unable_to_connect(hass, dummy_client): dummy_client.start.side_effect = AsyncMock(side_effect=ConnectionFailed) result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=MOCK_DISCOVER, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "confirm" @@ -107,7 +113,9 @@ async def test_ssdp_update(hass): entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=MOCK_DISCOVER, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -119,7 +127,9 @@ async def test_user(hass, aioclient_mock): """Test a manual user configuration flow.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=None, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=None, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -149,7 +159,9 @@ async def test_invalid_ssdp(hass, aioclient_mock): aioclient_mock.get(MOCK_UPNP_LOCATION, text="") result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=user_input, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" @@ -166,7 +178,9 @@ async def test_user_wrong(hass, aioclient_mock): aioclient_mock.get(MOCK_UPNP_LOCATION, status=404) result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=user_input, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py index 7d0aca8628e..2a119fd2017 100644 --- a/tests/components/arcam_fmj/test_device_trigger.py +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -38,7 +38,8 @@ async def test_get_triggers(hass, device_reg, entity_reg): config_entry = MockConfigEntry(domain=DOMAIN, data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "host", 1234)}, + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "host", 1234)}, ) entity_reg.async_get_or_create( "media_player", DOMAIN, "5678", device_id=device_entry.id @@ -83,7 +84,10 @@ async def test_if_fires_on_turn_on_request(hass, calls, player_setup, state): ) await hass.services.async_call( - "media_player", "turn_on", {"entity_id": player_setup}, blocking=True, + "media_player", + "turn_on", + {"entity_id": player_setup}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py index 0ccb67d0eb5..48418050d2d 100644 --- a/tests/components/atag/test_climate.py +++ b/tests/components/atag/test_climate.py @@ -80,7 +80,8 @@ async def test_setting_climate( async def test_incorrect_modes( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test incorrect values are handled correctly.""" with patch( diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index dc675e24eba..63cac2a0e9c 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -38,7 +38,8 @@ async def test_adding_second_device( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" with patch( - "pyatag.AtagOne.id", new_callable=PropertyMock(return_value="secondary_device"), + "pyatag.AtagOne.id", + new_callable=PropertyMock(return_value="secondary_device"), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT @@ -50,7 +51,9 @@ async def test_connection_error(hass): """Test we show user form on Atag connection error.""" with patch("pyatag.AtagOne.authorize", side_effect=errors.AtagException()): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -62,7 +65,9 @@ async def test_unauthorized(hass): """Test we show correct form when Unauthorized error is raised.""" with patch("pyatag.AtagOne.authorize", side_effect=errors.Unauthorized()): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -74,13 +79,17 @@ async def test_full_flow_implementation( ) -> None: """Test registering an integration and finishing flow works.""" aioclient_mock.get( - "http://127.0.0.1:10000/retrieve", json=RECEIVE_REPLY, + "http://127.0.0.1:10000/retrieve", + json=RECEIVE_REPLY, ) aioclient_mock.post( - "http://127.0.0.1:10000/pair", json=PAIR_REPLY, + "http://127.0.0.1:10000/pair", + json=PAIR_REPLY, ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == UID diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index ed75bc3685c..2f20347acae 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -34,7 +34,8 @@ async def test_form(hass): ), patch( "homeassistant.components.august.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.august.async_setup_entry", return_value=True, + "homeassistant.components.august.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -149,7 +150,8 @@ async def test_form_needs_validate(hass): "homeassistant.components.august.async_setup_entry", return_value=True ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], {VERIFICATION_CODE_KEY: "incorrect"}, + result["flow_id"], + {VERIFICATION_CODE_KEY: "incorrect"}, ) # Make sure we do not resend the code again @@ -176,7 +178,8 @@ async def test_form_needs_validate(hass): "homeassistant.components.august.async_setup_entry", return_value=True ) as mock_setup_entry: result4 = await hass.config_entries.flow.async_configure( - result["flow_id"], {VERIFICATION_CODE_KEY: "correct"}, + result["flow_id"], + {VERIFICATION_CODE_KEY: "correct"}, ) assert len(mock_send_verification_code.mock_calls) == 0 diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index f29403c9f21..bcc05e51c71 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -42,7 +42,9 @@ async def test_august_is_offline(hass): """Config entry state is ENTRY_STATE_SETUP_RETRY when august is offline.""" config_entry = MockConfigEntry( - domain=DOMAIN, data=_mock_get_config()[DOMAIN], title="August august", + domain=DOMAIN, + data=_mock_get_config()[DOMAIN], + title="August august", ) config_entry.add_to_hass(hass) @@ -146,7 +148,8 @@ async def test_set_up_from_yaml(hass): await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.august.async_setup_august", return_value=True, + "homeassistant.components.august.async_setup_august", + return_value=True, ) as mock_setup_august, patch( "homeassistant.components.august.config_flow.AugustGateway.async_authenticate", return_value=True, diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index e455960fef3..f36a5e3f180 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -105,7 +105,8 @@ async def test_one_lock_operation(hass): async def test_one_lock_unknown_state(hass): """Test creation of a lock with doorsense and bridge.""" lock_one = await _mock_lock_from_fixture( - hass, "get_lock.online.unknown_state.json", + hass, + "get_lock.online.unknown_state.json", ) await _create_august_with_devices(hass, [lock_one]) diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 8a02502e16c..e8aabce4678 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -166,7 +166,8 @@ async def test_find_link_tag_max_size(hass, mock_session): @pytest.mark.parametrize( - "client_id", ["https://home-assistant.io/android", "https://home-assistant.io/iOS"], + "client_id", + ["https://home-assistant.io/android", "https://home-assistant.io/iOS"], ) async def test_verify_redirect_uri_android_ios(client_id): """Test that we verify redirect uri correctly for Android/iOS.""" diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7179fbdaae2..d1c2e47a39e 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -584,7 +584,11 @@ async def test_automation_stops(hass, calls, service): ], } } - assert await async_setup_component(hass, automation.DOMAIN, config,) + assert await async_setup_component( + hass, + automation.DOMAIN, + config, + ) running = asyncio.Event() diff --git a/tests/components/avri/test_config_flow.py b/tests/components/avri/test_config_flow.py index c05935c62e4..2dba3c3c11d 100644 --- a/tests/components/avri/test_config_flow.py +++ b/tests/components/avri/test_config_flow.py @@ -15,7 +15,8 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.avri.async_setup_entry", return_value=True, + "homeassistant.components.avri.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index c36780a1803..63253993117 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -52,7 +52,8 @@ async def test_duplicate_error(hass): with patch( "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", return_value=True, + "homeassistant.components.awair.sensor.async_setup_entry", + return_value=True, ): MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG).add_to_hass( hass @@ -86,7 +87,8 @@ async def test_import(hass): with patch( "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", return_value=True, + "homeassistant.components.awair.sensor.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -120,7 +122,8 @@ async def test_import_aborts_if_configured(hass): with patch( "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", return_value=True, + "homeassistant.components.awair.sensor.async_setup_entry", + return_value=True, ): MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG).add_to_hass( hass @@ -141,7 +144,8 @@ async def test_reauth(hass): with patch( "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", return_value=True, + "homeassistant.components.awair.sensor.async_setup_entry", + return_value=True, ): mock_config = MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG) mock_config.add_to_hass(hass) @@ -150,7 +154,9 @@ async def test_reauth(hass): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG, + DOMAIN, + context={"source": "reauth", "unique_id": UNIQUE_ID}, + data=CONFIG, ) assert result["type"] == "abort" @@ -158,14 +164,18 @@ async def test_reauth(hass): with patch("python_awair.AwairClient.query", side_effect=AuthError()): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG, + DOMAIN, + context={"source": "reauth", "unique_id": UNIQUE_ID}, + data=CONFIG, ) assert result["errors"] == {CONF_ACCESS_TOKEN: "auth"} with patch("python_awair.AwairClient.query", side_effect=AwairError()): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG, + DOMAIN, + context={"source": "reauth", "unique_id": UNIQUE_ID}, + data=CONFIG, ) assert result["type"] == "abort" @@ -178,7 +188,8 @@ async def test_create_entry(hass): with patch( "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", return_value=True, + "homeassistant.components.awair.sensor.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index df0ee32389a..e5a32c4489a 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -68,7 +68,8 @@ async def test_manual_configuration_update_configuration(hass): assert result["step_id"] == "user" with patch( - "homeassistant.components.axis.async_setup_entry", return_value=True, + "homeassistant.components.axis.async_setup_entry", + return_value=True, ) as mock_setup_entry, patch( "axis.vapix.session_request", new=vapix_session_request ): @@ -170,11 +171,13 @@ async def test_flow_fails_device_unavailable(hass): async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): """Test that create entry can generate a name with other entries.""" entry = MockConfigEntry( - domain=AXIS_DOMAIN, data={CONF_NAME: "M1065-LW 0", CONF_MODEL: "M1065-LW"}, + domain=AXIS_DOMAIN, + data={CONF_NAME: "M1065-LW 0", CONF_MODEL: "M1065-LW"}, ) entry.add_to_hass(hass) entry2 = MockConfigEntry( - domain=AXIS_DOMAIN, data={CONF_NAME: "M1065-LW 1", CONF_MODEL: "M1065-LW"}, + domain=AXIS_DOMAIN, + data={CONF_NAME: "M1065-LW 1", CONF_MODEL: "M1065-LW"}, ) entry2.add_to_hass(hass) @@ -289,7 +292,8 @@ async def test_zeroconf_flow_updated_configuration(hass): } with patch( - "homeassistant.components.axis.async_setup_entry", return_value=True, + "homeassistant.components.axis.async_setup_entry", + return_value=True, ) as mock_setup_entry, patch( "axis.vapix.session_request", new=vapix_session_request ): @@ -359,7 +363,8 @@ async def test_option_flow(hass): } result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_STREAM_PROFILE: "profile_1"}, + result["flow_id"], + user_input={CONF_STREAM_PROFILE: "profile_1"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index c8583a8ce03..c6d78ca66e9 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -213,7 +213,8 @@ async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTION config_entry.add_to_hass(hass) with patch("axis.vapix.session_request", new=vapix_session_request), patch( - "axis.rtsp.RTSPClient.start", return_value=True, + "axis.rtsp.RTSPClient.start", + return_value=True, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -291,7 +292,8 @@ async def test_update_address(hass): assert device.api.config.host == "1.2.3.4" with patch("axis.vapix.session_request", new=vapix_session_request), patch( - "homeassistant.components.axis.async_setup_entry", return_value=True, + "homeassistant.components.axis.async_setup_entry", + return_value=True, ) as mock_setup_entry: await hass.config_entries.flow.async_init( AXIS_DOMAIN, diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index b89c9cb69aa..fce4c6f5df3 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -44,7 +44,8 @@ async def test_authorization_error(hass: HomeAssistant) -> None: assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT, + result["flow_id"], + FIXTURE_USER_INPUT, ) await hass.async_block_till_done() @@ -67,7 +68,8 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_REAUTH_INPUT, + result["flow_id"], + FIXTURE_REAUTH_INPUT, ) await hass.async_block_till_done() @@ -90,7 +92,8 @@ async def test_connection_error(hass: HomeAssistant) -> None: assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT, + result["flow_id"], + FIXTURE_USER_INPUT, ) await hass.async_block_till_done() @@ -113,7 +116,8 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_REAUTH_INPUT, + result["flow_id"], + FIXTURE_REAUTH_INPUT, ) await hass.async_block_till_done() @@ -141,7 +145,8 @@ async def test_project_error(hass: HomeAssistant) -> None: assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT, + result["flow_id"], + FIXTURE_USER_INPUT, ) await hass.async_block_till_done() @@ -169,7 +174,8 @@ async def test_reauth_project_error(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_REAUTH_INPUT, + result["flow_id"], + FIXTURE_REAUTH_INPUT, ) await hass.async_block_till_done() @@ -209,7 +215,8 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ), ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_REAUTH_INPUT, + result["flow_id"], + FIXTURE_REAUTH_INPUT, ) await hass.async_block_till_done() @@ -222,7 +229,8 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: with patch( "homeassistant.components.azure_devops.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.azure_devops.async_setup_entry", return_value=True, + "homeassistant.components.azure_devops.async_setup_entry", + return_value=True, ) as mock_setup_entry, patch( "homeassistant.components.azure_devops.config_flow.DevOpsClient.authorized", return_value=True, @@ -242,7 +250,8 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT, + result["flow_id"], + FIXTURE_USER_INPUT, ) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index a40df84b899..99af954cd4d 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -10,11 +10,13 @@ def test_state(): sensor = binary_sensor.BinarySensorEntity() assert STATE_OFF == sensor.state with mock.patch( - "homeassistant.components.binary_sensor.BinarySensorEntity.is_on", new=False, + "homeassistant.components.binary_sensor.BinarySensorEntity.is_on", + new=False, ): assert STATE_OFF == binary_sensor.BinarySensorEntity().state with mock.patch( - "homeassistant.components.binary_sensor.BinarySensorEntity.is_on", new=True, + "homeassistant.components.binary_sensor.BinarySensorEntity.is_on", + new=True, ): assert STATE_ON == binary_sensor.BinarySensorEntity().state diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py index e7c20b10b2c..cd060445bf7 100644 --- a/tests/components/blebox/test_cover.py +++ b/tests/components/blebox/test_cover.py @@ -210,7 +210,10 @@ async def test_open(feature, hass, config): feature_mock.async_update = AsyncMock() await hass.services.async_call( - "cover", SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True, + "cover", + SERVICE_OPEN_COVER, + {"entity_id": entity_id}, + blocking=True, ) assert hass.states.get(entity_id).state == STATE_OPENING diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index fb0d1302e33..48af534545c 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -116,7 +116,10 @@ async def test_dimmer_on(dimmer, hass, config): feature_mock.async_on = AsyncMock(side_effect=turn_on) await hass.services.async_call( - "light", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, ) state = hass.states.get(entity_id) @@ -186,7 +189,10 @@ async def test_dimmer_off(dimmer, hass, config): feature_mock.async_off = AsyncMock(side_effect=turn_off) await hass.services.async_call( - "light", SERVICE_TURN_OFF, {"entity_id": entity_id}, blocking=True, + "light", + SERVICE_TURN_OFF, + {"entity_id": entity_id}, + blocking=True, ) state = hass.states.get(entity_id) @@ -282,7 +288,10 @@ async def test_wlightbox_s_on(wlightbox_s, hass, config): feature_mock.async_on = AsyncMock(side_effect=turn_on) await hass.services.async_call( - "light", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, ) state = hass.states.get(entity_id) @@ -523,7 +532,10 @@ async def test_wlightbox_on_to_last_color(wlightbox, hass, config): feature_mock.sensible_on_value = "f1e2d3e4" await hass.services.async_call( - "light", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, ) state = hass.states.get(entity_id) @@ -555,7 +567,10 @@ async def test_wlightbox_off(wlightbox, hass, config): feature_mock.async_off = AsyncMock(side_effect=turn_off) await hass.services.async_call( - "light", SERVICE_TURN_OFF, {"entity_id": entity_id}, blocking=True, + "light", + SERVICE_TURN_OFF, + {"entity_id": entity_id}, + blocking=True, ) state = hass.states.get(entity_id) @@ -589,7 +604,10 @@ async def test_turn_on_failure(feature, hass, config, caplog): feature_mock.sensible_on_value = 123 await hass.services.async_call( - "light", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, ) assert ( diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py index c41273757f2..43d7d811acc 100644 --- a/tests/components/blebox/test_switch.py +++ b/tests/components/blebox/test_switch.py @@ -117,7 +117,10 @@ async def test_switchbox_on(switchbox, hass, config): feature_mock.async_turn_on = AsyncMock(side_effect=turn_on) await hass.services.async_call( - "switch", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + "switch", + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, ) state = hass.states.get(entity_id) @@ -142,7 +145,10 @@ async def test_switchbox_off(switchbox, hass, config): feature_mock.async_turn_off = AsyncMock(side_effect=turn_off) await hass.services.async_call( - "switch", SERVICE_TURN_OFF, {"entity_id": entity_id}, blocking=True, + "switch", + SERVICE_TURN_OFF, + {"entity_id": entity_id}, + blocking=True, ) state = hass.states.get(entity_id) assert state.state == STATE_OFF @@ -279,7 +285,10 @@ async def test_switchbox_d_turn_first_on(switchbox_d, hass, config): feature_mocks[0].async_turn_on = AsyncMock(side_effect=turn_on0) await hass.services.async_call( - "switch", SERVICE_TURN_ON, {"entity_id": entity_ids[0]}, blocking=True, + "switch", + SERVICE_TURN_ON, + {"entity_id": entity_ids[0]}, + blocking=True, ) assert hass.states.get(entity_ids[0]).state == STATE_ON @@ -305,7 +314,10 @@ async def test_switchbox_d_second_on(switchbox_d, hass, config): feature_mocks[1].async_turn_on = AsyncMock(side_effect=turn_on1) await hass.services.async_call( - "switch", SERVICE_TURN_ON, {"entity_id": entity_ids[1]}, blocking=True, + "switch", + SERVICE_TURN_ON, + {"entity_id": entity_ids[1]}, + blocking=True, ) assert hass.states.get(entity_ids[0]).state == STATE_OFF @@ -331,7 +343,10 @@ async def test_switchbox_d_first_off(switchbox_d, hass, config): feature_mocks[0].async_turn_off = AsyncMock(side_effect=turn_off0) await hass.services.async_call( - "switch", SERVICE_TURN_OFF, {"entity_id": entity_ids[0]}, blocking=True, + "switch", + SERVICE_TURN_OFF, + {"entity_id": entity_ids[0]}, + blocking=True, ) assert hass.states.get(entity_ids[0]).state == STATE_OFF @@ -357,7 +372,10 @@ async def test_switchbox_d_second_off(switchbox_d, hass, config): feature_mocks[1].async_turn_off = AsyncMock(side_effect=turn_off1) await hass.services.async_call( - "switch", SERVICE_TURN_OFF, {"entity_id": entity_ids[1]}, blocking=True, + "switch", + SERVICE_TURN_OFF, + {"entity_id": entity_ids[1]}, + blocking=True, ) assert hass.states.get(entity_ids[0]).state == STATE_ON assert hass.states.get(entity_ids[1]).state == STATE_OFF diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 99b20d9a73c..72a4a5272ed 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -24,10 +24,12 @@ async def test_form(hass): ), patch( "homeassistant.components.blink.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.blink.async_setup_entry", return_value=True, + "homeassistant.components.blink.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"username": "blink@example.com", "password": "example"}, + result["flow_id"], + {"username": "blink@example.com", "password": "example"}, ) assert result2["type"] == "create_entry" @@ -62,7 +64,8 @@ async def test_form_2fa(hass): "homeassistant.components.blink.async_setup", return_value=True ) as mock_setup: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"username": "blink@example.com", "password": "example"}, + result["flow_id"], + {"username": "blink@example.com", "password": "example"}, ) assert result2["type"] == "form" @@ -106,7 +109,8 @@ async def test_form_2fa_connect_error(hass): return_value=True, ), patch("homeassistant.components.blink.async_setup", return_value=True): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"username": "blink@example.com", "password": "example"}, + result["flow_id"], + {"username": "blink@example.com", "password": "example"}, ) assert result2["type"] == "form" @@ -146,7 +150,8 @@ async def test_form_2fa_invalid_key(hass): return_value=True, ), patch("homeassistant.components.blink.async_setup", return_value=True): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"username": "blink@example.com", "password": "example"}, + result["flow_id"], + {"username": "blink@example.com", "password": "example"}, ) assert result2["type"] == "form" @@ -186,7 +191,8 @@ async def test_form_2fa_unknown_error(hass): return_value=True, ), patch("homeassistant.components.blink.async_setup", return_value=True): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"username": "blink@example.com", "password": "example"}, + result["flow_id"], + {"username": "blink@example.com", "password": "example"}, ) assert result2["type"] == "form" @@ -239,7 +245,8 @@ async def test_form_unknown_error(hass): ) with patch( - "homeassistant.components.blink.config_flow.Auth.startup", side_effect=KeyError, + "homeassistant.components.blink.config_flow.Auth.startup", + side_effect=KeyError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"username": "blink@example.com", "password": "example"} @@ -288,7 +295,8 @@ async def test_options_flow(hass): assert result["step_id"] == "simple_options" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"scan_interval": 5}, + result["flow_id"], + user_input={"scan_interval": 5}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 31308746555..d5e4a0b9418 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -113,7 +113,8 @@ def patch_bond_device_ids(enabled: bool = True, return_value=None, side_effect=N def patch_bond_device(return_value=None): """Patch Bond API device endpoint.""" return patch( - "homeassistant.components.bond.Bond.device", return_value=return_value, + "homeassistant.components.bond.Bond.device", + return_value=return_value, ) diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 8671cc4b601..cd98dd8090a 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -162,7 +162,8 @@ async def test_zeroconf_form(hass: core.HomeAssistant): return_value={"bondid": "test-bond-id"} ), patch_bond_device_ids(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_ACCESS_TOKEN: "test-token"}, + result["flow_id"], + {CONF_ACCESS_TOKEN: "test-token"}, ) assert result2["type"] == "create_entry" @@ -249,4 +250,7 @@ def _patch_async_setup(): def _patch_async_setup_entry(): - return patch("homeassistant.components.bond.async_setup_entry", return_value=True,) + return patch( + "homeassistant.components.bond.async_setup_entry", + return_value=True, + ) diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index fa7e59e30e6..72770817110 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -45,7 +45,10 @@ async def turn_fan_on( if speed: service_data[fan.ATTR_SPEED] = speed await hass.services.async_call( - FAN_DOMAIN, SERVICE_TURN_ON, service_data=service_data, blocking=True, + FAN_DOMAIN, + SERVICE_TURN_ON, + service_data=service_data, + blocking=True, ) await hass.async_block_till_done() @@ -149,7 +152,10 @@ async def test_turn_off_fan(hass: core.HomeAssistant): with patch_bond_action() as mock_turn_off, patch_bond_device_state(): await hass.services.async_call( - FAN_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "fan.name_1"}, blocking=True, + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "fan.name_1"}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 7a0f057f17b..3dd54f0de6f 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -27,7 +27,8 @@ async def test_async_setup_no_domain_config(hass: HomeAssistant): async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant): """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, + domain=DOMAIN, + data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) config_entry.add_to_hass(hass) @@ -39,7 +40,8 @@ async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant): async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAssistant): """Test that configuring entry sets up cover domain.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, + domain=DOMAIN, + data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) with patch_bond_version( @@ -86,7 +88,8 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss async def test_unload_config_entry(hass: HomeAssistant): """Test that configuration entry supports unloading.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, + domain=DOMAIN, + data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ) result = await setup_bond_entity( diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index 57ea859240a..17b8c87978f 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -165,7 +165,10 @@ async def test_brightness_support(hass: core.HomeAssistant): async def test_brightness_not_supported(hass: core.HomeAssistant): """Tests that a non-dimmable light should not support the brightness feature.""" await setup_platform( - hass, LIGHT_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id", + hass, + LIGHT_DOMAIN, + ceiling_fan("name-1"), + bond_device_id="test-device-id", ) state = hass.states.get("light.name_1") diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 43237cd70ba..4089c551ff5 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -40,7 +40,8 @@ async def test_flow_user_works(hass): with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) assert result["type"] == "form" @@ -48,7 +49,8 @@ async def test_flow_user_works(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"name": device.name}, + result["flow_id"], + {"name": device.name}, ) assert result["type"] == "create_entry" @@ -69,7 +71,8 @@ async def test_flow_user_already_in_progress(hass): with patch("broadlink.discover", return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) result = await hass.config_entries.flow.async_init( @@ -78,7 +81,8 @@ async def test_flow_user_already_in_progress(hass): with patch("broadlink.discover", return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) assert result["type"] == "abort" @@ -104,7 +108,8 @@ async def test_flow_user_mac_already_configured(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) assert result["type"] == "abort" @@ -122,7 +127,8 @@ async def test_flow_user_invalid_ip_address(hass): with patch("broadlink.discover", side_effect=OSError(errno.EINVAL, None)): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "0.0.0.1"}, + result["flow_id"], + {"host": "0.0.0.1"}, ) assert result["type"] == "form" @@ -138,7 +144,8 @@ async def test_flow_user_invalid_hostname(hass): with patch("broadlink.discover", side_effect=OSError(socket.EAI_NONAME, None)): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "pancakemaster.local"}, + result["flow_id"], + {"host": "pancakemaster.local"}, ) assert result["type"] == "form" @@ -156,7 +163,8 @@ async def test_flow_user_device_not_found(hass): with patch("broadlink.discover", return_value=[]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host}, + result["flow_id"], + {"host": device.host}, ) assert result["type"] == "form" @@ -172,7 +180,8 @@ async def test_flow_user_network_unreachable(hass): with patch("broadlink.discover", side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "192.168.1.32"}, + result["flow_id"], + {"host": "192.168.1.32"}, ) assert result["type"] == "form" @@ -188,7 +197,8 @@ async def test_flow_user_os_error(hass): with patch("broadlink.discover", side_effect=OSError()): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "192.168.1.32"}, + result["flow_id"], + {"host": "192.168.1.32"}, ) assert result["type"] == "form" @@ -208,7 +218,8 @@ async def test_flow_auth_authentication_error(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) assert result["type"] == "form" @@ -228,7 +239,8 @@ async def test_flow_auth_device_offline(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host}, + result["flow_id"], + {"host": device.host}, ) assert result["type"] == "form" @@ -248,7 +260,8 @@ async def test_flow_auth_firmware_error(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host}, + result["flow_id"], + {"host": device.host}, ) assert result["type"] == "form" @@ -268,7 +281,8 @@ async def test_flow_auth_network_unreachable(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host}, + result["flow_id"], + {"host": device.host}, ) assert result["type"] == "form" @@ -288,7 +302,8 @@ async def test_flow_auth_os_error(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host}, + result["flow_id"], + {"host": device.host}, ) assert result["type"] == "form" @@ -308,16 +323,19 @@ async def test_flow_reset_works(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) with patch("broadlink.discover", return_value=[device.get_mock_api()]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"name": device.name}, + result["flow_id"], + {"name": device.name}, ) assert result["type"] == "create_entry" @@ -337,7 +355,8 @@ async def test_flow_unlock_works(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) assert result["type"] == "form" @@ -345,11 +364,13 @@ async def test_flow_unlock_works(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"unlock": True}, + result["flow_id"], + {"unlock": True}, ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"name": device.name}, + result["flow_id"], + {"name": device.name}, ) assert result["type"] == "create_entry" @@ -373,11 +394,13 @@ async def test_flow_unlock_device_offline(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"unlock": True}, + result["flow_id"], + {"unlock": True}, ) assert result["type"] == "form" @@ -398,11 +421,13 @@ async def test_flow_unlock_firmware_error(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"unlock": True}, + result["flow_id"], + {"unlock": True}, ) assert result["type"] == "form" @@ -423,11 +448,13 @@ async def test_flow_unlock_network_unreachable(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"unlock": True}, + result["flow_id"], + {"unlock": True}, ) assert result["type"] == "form" @@ -448,11 +475,13 @@ async def test_flow_unlock_os_error(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"unlock": True}, + result["flow_id"], + {"unlock": True}, ) assert result["type"] == "form" @@ -472,15 +501,18 @@ async def test_flow_do_not_unlock(hass): with patch("broadlink.discover", return_value=[mock_api]): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"unlock": False}, + result["flow_id"], + {"unlock": False}, ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"name": device.name}, + result["flow_id"], + {"name": device.name}, ) assert result["type"] == "create_entry" @@ -507,7 +539,8 @@ async def test_flow_import_works(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"name": device.name}, + result["flow_id"], + {"name": device.name}, ) assert result["type"] == "create_entry" @@ -671,7 +704,8 @@ async def test_flow_reauth_works(hass): with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) assert result["type"] == "abort" @@ -704,7 +738,8 @@ async def test_flow_reauth_invalid_host(hass): with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) assert result["type"] == "form" @@ -737,7 +772,8 @@ async def test_flow_reauth_valid_host(hass): with patch("broadlink.discover", return_value=[mock_api]) as mock_discover: result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": device.host, "timeout": device.timeout}, + result["flow_id"], + {"host": device.host, "timeout": device.timeout}, ) assert result["type"] == "abort" diff --git a/tests/components/bsblan/__init__.py b/tests/components/bsblan/__init__.py index 1541555de55..45b0f16c0a1 100644 --- a/tests/components/bsblan/__init__.py +++ b/tests/components/bsblan/__init__.py @@ -13,7 +13,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def init_integration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + skip_setup: bool = False, ) -> MockConfigEntry: """Set up the BSBLan integration in Home Assistant.""" diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index 48f71a8404f..c24fbc34f6a 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -17,7 +17,8 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + config_flow.DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" @@ -70,7 +71,8 @@ async def test_full_user_flow_implementation( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + config_flow.DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 8a79cb39ec9..c3db5067b1f 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -27,7 +27,8 @@ async def mock_camera_fixture(hass): await hass.async_block_till_done() with patch( - "homeassistant.components.demo.camera.Path.read_bytes", return_value=b"Test", + "homeassistant.components.demo.camera.Path.read_bytes", + return_value=b"Test", ): yield @@ -258,7 +259,8 @@ async def test_play_stream_service_no_source(hass, mock_camera, mock_stream): async def test_handle_play_stream_service(hass, mock_camera, mock_stream): """Test camera play_stream service.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) await async_setup_component(hass, "media_player", {}) with patch( diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 8a3a935429f..2842ddc23f0 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -9,7 +9,8 @@ from tests.common import MockConfigEntry, async_mock_signal async def test_service_show_view(hass): """Test we don't set app id in prod.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) @@ -35,7 +36,8 @@ async def test_service_show_view(hass): async def test_service_show_view_dashboard(hass): """Test casting a specific dashboard.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) @@ -61,7 +63,8 @@ async def test_service_show_view_dashboard(hass): async def test_use_cloud_url(hass): """Test that we fall back to cloud url.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) hass.config.components.add("cloud") diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 5d8a54dc691..d3348241ec8 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -629,7 +629,8 @@ async def test_failed_cast_other_url(hass, caplog): async def test_failed_cast_internal_url(hass, caplog): """Test warning when casting from internal_url fails.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) with assert_setup_component(1, tts.DOMAIN): assert await async_setup_component( @@ -653,7 +654,8 @@ async def test_failed_cast_internal_url(hass, caplog): async def test_failed_cast_external_url(hass, caplog): """Test warning when casting from external_url fails.""" await async_process_ha_core_config( - hass, {"external_url": "http://example.com:8123"}, + hass, + {"external_url": "http://example.com:8123"}, ) with assert_setup_component(1, tts.DOMAIN): assert await async_setup_component( diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 13c4649f39e..aa59e935a86 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -462,7 +462,8 @@ async def test_websocket_subscription_not_logged_in(hass, hass_ws_client): """Test querying the status.""" client = await hass_ws_client(hass) with patch( - "hass_nabucasa.Cloud.fetch_subscription_info", return_value={"return": "value"}, + "hass_nabucasa.Cloud.fetch_subscription_info", + return_value={"return": "value"}, ): await client.send_json({"id": 5, "type": "cloud/subscription"}) response = await client.receive_json() diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 4707296a426..ceb3711f62b 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -111,11 +111,16 @@ async def test_reload(hass): assert hass.states.get("cover.test").state yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "command_line/configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "command_line/configuration.yaml", ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "command_line", SERVICE_RELOAD, {}, blocking=True, + "command_line", + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 623269b9c16..0a6e29ca00c 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -78,7 +78,9 @@ class TestCommandSensorSensor(unittest.TestCase): return_value=b"Works\n", ) as check_output: data = command_line.CommandSensorData( - self.hass, 'echo "{{ states.sensor.test_state.state }}" "3 4"', 15, + self.hass, + 'echo "{{ states.sensor.test_state.state }}" "3 4"', + 15, ) data.update() diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 9bd8875add0..11299f4108b 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -406,7 +406,8 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): hass_admin_user.groups = [] resp = await client.post( - f"/api/config/config_entries/flow/{flow_id}", json={"user_title": "user-title"}, + f"/api/config/config_entries/flow/{flow_id}", + json={"user_title": "user-title"}, ) assert resp.status == 401 diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 6a7447d9409..1f379727b44 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -125,7 +125,8 @@ async def test_websocket_bad_core_update(hass, client): async def test_detect_config(hass, client): """Test detect config.""" with patch( - "homeassistant.util.location.async_detect_location_info", return_value=None, + "homeassistant.util.location.async_detect_location_info", + return_value=None, ): await client.send_json({"id": 1, "type": "config/core/detect"}) diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index 30a475dab77..ca3bcc98c7d 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -55,7 +55,8 @@ async def test_update_entity(hass, hass_client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write ), patch( - "homeassistant.config.async_hass_config_yaml", return_value={}, + "homeassistant.config.async_hass_config_yaml", + return_value={}, ): resp = await client.post( "/api/config/customize/config/hello.world", diff --git a/tests/components/conftest.py b/tests/components/conftest.py index e78a67a16f3..acfbdeb8629 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -15,6 +15,7 @@ zeroconf.install_multiple_zeroconf_catcher = lambda zc: None def prevent_io(): """Fixture to prevent certain I/O from happening.""" with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", return_value=[], + "homeassistant.components.http.ban.async_load_ip_bans_config", + return_value=[], ): yield diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index 6d3293b147a..f87c5af3484 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -64,7 +64,8 @@ async def test_form(hass): ), patch( "homeassistant.components.control4.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.control4.async_setup_entry", return_value=True, + "homeassistant.components.control4.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -171,7 +172,8 @@ async def test_option_flow(hass): assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SCAN_INTERVAL: 4}, + result["flow_id"], + user_input={CONF_SCAN_INTERVAL: 4}, ) assert result["type"] == "create_entry" assert result["data"] == { diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index 88985f7f88a..fc296a53ddf 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -26,7 +26,8 @@ async def test_form(hass): ), patch( "homeassistant.components.coolmaster.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.coolmaster.async_setup_entry", return_value=True, + "homeassistant.components.coolmaster.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], _flow_data() diff --git a/tests/components/coronavirus/test_config_flow.py b/tests/components/coronavirus/test_config_flow.py index b7af2e343b2..06d586ba2a5 100644 --- a/tests/components/coronavirus/test_config_flow.py +++ b/tests/components/coronavirus/test_config_flow.py @@ -13,7 +13,8 @@ async def test_form(hass): assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"country": OPTION_WORLDWIDE}, + result["flow_id"], + {"country": OPTION_WORLDWIDE}, ) assert result2["type"] == "create_entry" assert result2["title"] == "Worldwide" diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index c315bcc32a8..324c73dfb81 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -54,14 +54,17 @@ def mock_daikin_discovery(): async def test_user(hass, mock_daikin): """Test user config.""" result = await hass.config_entries.flow.async_init( - "daikin", context={"source": SOURCE_USER}, + "daikin", + context={"source": SOURCE_USER}, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( - "daikin", context={"source": SOURCE_USER}, data={CONF_HOST: HOST}, + "daikin", + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST}, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == HOST @@ -73,7 +76,9 @@ async def test_abort_if_already_setup(hass, mock_daikin): """Test we abort if Daikin is already setup.""" MockConfigEntry(domain="daikin", unique_id=MAC).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "daikin", context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, + "daikin", + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, KEY_MAC: MAC}, ) assert result["type"] == RESULT_TYPE_ABORT @@ -83,13 +88,17 @@ async def test_abort_if_already_setup(hass, mock_daikin): async def test_import(hass, mock_daikin): """Test import step.""" result = await hass.config_entries.flow.async_init( - "daikin", context={"source": SOURCE_IMPORT}, data={}, + "daikin", + context={"source": SOURCE_IMPORT}, + data={}, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( - "daikin", context={"source": SOURCE_IMPORT}, data={CONF_HOST: HOST}, + "daikin", + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: HOST}, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == HOST @@ -111,7 +120,9 @@ async def test_device_abort(hass, mock_daikin, s_effect, reason): mock_daikin.factory.side_effect = s_effect result = await hass.config_entries.flow.async_init( - "daikin", context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, + "daikin", + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, KEY_MAC: MAC}, ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": reason} @@ -130,7 +141,9 @@ async def test_discovery_zeroconf( ): """Test discovery/zeroconf step.""" result = await hass.config_entries.flow.async_init( - "daikin", context={"source": source}, data=data, + "daikin", + context={"source": source}, + data=data, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -146,7 +159,9 @@ async def test_discovery_zeroconf( assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( - "daikin", context={"source": source}, data=data, + "daikin", + context={"source": source}, + data=data, ) assert result["type"] == RESULT_TYPE_ABORT diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index da4076944d0..06788728115 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -161,7 +161,10 @@ class TestDatadog(unittest.TestCase): ) assert mock_client.gauge.call_args == mock.call( - "ha.sensor", out, sample_rate=1, tags=[f"entity:{state.entity_id}"], + "ha.sensor", + out, + sample_rate=1, + tags=[f"entity:{state.entity_id}"], ) mock_client.gauge.reset_mock() diff --git a/tests/components/debugpy/test_init.py b/tests/components/debugpy/test_init.py index 1de8da9ac9a..86be0d788f6 100644 --- a/tests/components/debugpy/test_init.py +++ b/tests/components/debugpy/test_init.py @@ -53,7 +53,9 @@ async def test_on_demand(hass: HomeAssistant, mock_debugpy) -> None: assert len(mock_debugpy.method_calls) == 0 await hass.services.async_call( - DOMAIN, SERVICE_START, blocking=True, + DOMAIN, + SERVICE_START, + blocking=True, ) mock_debugpy.listen.assert_called_once_with(("127.0.0.1", 80)) diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 0a536bcda5b..b51869b01ab 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -87,7 +87,8 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, + result["flow_id"], + user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -134,7 +135,8 @@ async def test_flow_manual_configuration(hass, aioclient_mock): assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, + result["flow_id"], + user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -209,7 +211,8 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 80}, + result["flow_id"], + user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 80}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -254,7 +257,8 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, + result["flow_id"], + user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -296,7 +300,8 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, + result["flow_id"], + user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -401,7 +406,8 @@ async def test_ssdp_discovery_update_configuration(hass): gateway = await setup_deconz_integration(hass) with patch( - "homeassistant.components.deconz.async_setup_entry", return_value=True, + "homeassistant.components.deconz.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -478,7 +484,8 @@ async def test_flow_hassio_discovery(hass): with patch( "homeassistant.components.deconz.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.deconz.async_setup_entry", return_value=True, + "homeassistant.components.deconz.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -500,7 +507,8 @@ async def test_hassio_discovery_update_configuration(hass): gateway = await setup_deconz_integration(hass) with patch( - "homeassistant.components.deconz.async_setup_entry", return_value=True, + "homeassistant.components.deconz.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 4236888a5a6..15ff304459b 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -136,7 +136,8 @@ async def test_update_address(hass): assert gateway.api.host == "1.2.3.4" with patch( - "homeassistant.components.deconz.async_setup_entry", return_value=True, + "homeassistant.components.deconz.async_setup_entry", + return_value=True, ) as mock_setup_entry: await hass.config_entries.flow.async_init( deconz.config_flow.DOMAIN, @@ -173,7 +174,8 @@ async def test_get_gateway(hass): async def test_get_gateway_fails_unauthorized(hass): """Failed call.""" with patch( - "pydeconz.DeconzSession.initialize", side_effect=pydeconz.errors.Unauthorized, + "pydeconz.DeconzSession.initialize", + side_effect=pydeconz.errors.Unauthorized, ), pytest.raises(deconz.errors.AuthenticationRequired): assert ( await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) @@ -184,7 +186,8 @@ async def test_get_gateway_fails_unauthorized(hass): async def test_get_gateway_fails_cannot_connect(hass): """Failed call.""" with patch( - "pydeconz.DeconzSession.initialize", side_effect=pydeconz.errors.RequestError, + "pydeconz.DeconzSession.initialize", + side_effect=pydeconz.errors.RequestError, ), pytest.raises(deconz.errors.CannotConnect): assert ( await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index f88ac38c46c..8fcc3af0f26 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -55,7 +55,8 @@ def denonavr_connect_fixture(): "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.get_device_info", return_value=True, ), patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.name", TEST_NAME, + "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.name", + TEST_NAME, ), patch( "homeassistant.components.denonavr.receiver.denonavr.DenonAVR.model_name", TEST_MODEL, @@ -92,7 +93,8 @@ async def test_config_flow_manual_host_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: TEST_HOST}, + result["flow_id"], + {CONF_HOST: TEST_HOST}, ) assert result["type"] == "create_entry" @@ -125,7 +127,10 @@ async def test_config_flow_manual_discover_1_success(hass): "homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers", return_value=TEST_DISCOVER_1_RECEIVER, ): - result = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result["type"] == "create_entry" assert result["title"] == TEST_NAME @@ -157,14 +162,18 @@ async def test_config_flow_manual_discover_2_success(hass): "homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers", return_value=TEST_DISCOVER_2_RECEIVER, ): - result = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result["type"] == "form" assert result["step_id"] == "select" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"select_host": TEST_HOST2}, + result["flow_id"], + {"select_host": TEST_HOST2}, ) assert result["type"] == "create_entry" @@ -197,7 +206,10 @@ async def test_config_flow_manual_discover_error(hass): "homeassistant.components.denonavr.config_flow.denonavr.ssdp.identify_denonavr_receivers", return_value=[], ): - result = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -223,7 +235,8 @@ async def test_config_flow_manual_host_no_serial(hass): None, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: TEST_HOST}, + result["flow_id"], + {CONF_HOST: TEST_HOST}, ) assert result["type"] == "create_entry" @@ -257,7 +270,8 @@ async def test_config_flow_manual_host_no_mac(hass): return_value=None, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: TEST_HOST}, + result["flow_id"], + {CONF_HOST: TEST_HOST}, ) assert result["type"] == "create_entry" @@ -294,7 +308,8 @@ async def test_config_flow_manual_host_no_serial_no_mac(hass): return_value=None, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: TEST_HOST}, + result["flow_id"], + {CONF_HOST: TEST_HOST}, ) assert result["type"] == "create_entry" @@ -331,7 +346,8 @@ async def test_config_flow_manual_host_no_serial_no_mac_exception(hass): side_effect=OSError, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: TEST_HOST}, + result["flow_id"], + {CONF_HOST: TEST_HOST}, ) assert result["type"] == "create_entry" @@ -368,7 +384,8 @@ async def test_config_flow_manual_host_connection_error(hass): None, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: TEST_HOST}, + result["flow_id"], + {CONF_HOST: TEST_HOST}, ) assert result["type"] == "abort" @@ -394,7 +411,8 @@ async def test_config_flow_manual_host_no_device_info(hass): None, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: TEST_HOST}, + result["flow_id"], + {CONF_HOST: TEST_HOST}, ) assert result["type"] == "abort" @@ -417,7 +435,10 @@ async def test_config_flow_ssdp(hass): assert result["type"] == "form" assert result["step_id"] == "confirm" - result = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result["type"] == "create_entry" assert result["title"] == TEST_NAME @@ -549,7 +570,8 @@ async def test_config_flow_manual_host_no_serial_double_config(hass): None, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: TEST_HOST}, + result["flow_id"], + {CONF_HOST: TEST_HOST}, ) assert result["type"] == "create_entry" @@ -576,7 +598,8 @@ async def test_config_flow_manual_host_no_serial_double_config(hass): None, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: TEST_HOST}, + result["flow_id"], + {CONF_HOST: TEST_HOST}, ) assert result["type"] == "abort" diff --git a/tests/components/denonavr/test_media_player.py b/tests/components/denonavr/test_media_player.py index 980ad758c80..bb9f83b58d7 100644 --- a/tests/components/denonavr/test_media_player.py +++ b/tests/components/denonavr/test_media_player.py @@ -36,7 +36,8 @@ ENTITY_ID = f"{media_player.DOMAIN}.{TEST_NAME}" def client_fixture(): """Patch of client library for tests.""" with patch( - "homeassistant.components.denonavr.receiver.denonavr.DenonAVR", autospec=True, + "homeassistant.components.denonavr.receiver.denonavr.DenonAVR", + autospec=True, ) as mock_client_class, patch( "homeassistant.components.denonavr.receiver.denonavr.discover" ): @@ -64,7 +65,9 @@ async def setup_denonavr(hass): } mock_entry = MockConfigEntry( - domain=DOMAIN, unique_id=TEST_UNIQUE_ID, data=entry_data, + domain=DOMAIN, + unique_id=TEST_UNIQUE_ID, + data=entry_data, ) mock_entry.add_to_hass(hass) diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 2663018fc09..19715577e2a 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -222,7 +222,9 @@ async def test_initialize_start(hass): """Test we initialize when HA starts.""" hass.state = CoreState.not_running assert await async_setup_component( - hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}}, + hass, + device_sun_light_trigger.DOMAIN, + {device_sun_light_trigger.DOMAIN: {}}, ) with patch( diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 01b4603257a..6790c85b777 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -17,7 +17,8 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.devolo_home_control.async_setup", return_value=True, + "homeassistant.components.devolo_home_control.async_setup", + return_value=True, ) as mock_setup, patch( "homeassistant.components.devolo_home_control.async_setup_entry", return_value=True, @@ -96,7 +97,8 @@ async def test_form_advanced_options(hass): assert result["errors"] == {} with patch( - "homeassistant.components.devolo_home_control.async_setup", return_value=True, + "homeassistant.components.devolo_home_control.async_setup", + return_value=True, ) as mock_setup, patch( "homeassistant.components.devolo_home_control.async_setup_entry", return_value=True, diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index b244751a811..b95f796b230 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -25,10 +25,12 @@ async def test_form(hass): ), patch( "homeassistant.components.dexcom.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.dexcom.async_setup_entry", return_value=True, + "homeassistant.components.dexcom.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG, + result["flow_id"], + CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -46,10 +48,12 @@ async def test_form_account_error(hass): ) with patch( - "homeassistant.components.dexcom.config_flow.Dexcom", side_effect=AccountError, + "homeassistant.components.dexcom.config_flow.Dexcom", + side_effect=AccountError, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG, + result["flow_id"], + CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -63,10 +67,12 @@ async def test_form_session_error(hass): ) with patch( - "homeassistant.components.dexcom.config_flow.Dexcom", side_effect=SessionError, + "homeassistant.components.dexcom.config_flow.Dexcom", + side_effect=SessionError, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG, + result["flow_id"], + CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -80,10 +86,12 @@ async def test_form_unknown_error(hass): ) with patch( - "homeassistant.components.dexcom.config_flow.Dexcom", side_effect=Exception, + "homeassistant.components.dexcom.config_flow.Dexcom", + side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG, + result["flow_id"], + CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -92,7 +100,11 @@ async def test_form_unknown_error(hass): async def test_option_flow_default(hass): """Test config flow options.""" - entry = MockConfigEntry(domain=DOMAIN, data=CONFIG, options=None,) + entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG, + options=None, + ) entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(entry.entry_id) @@ -101,7 +113,8 @@ async def test_option_flow_default(hass): assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={}, + result["flow_id"], + user_input={}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["data"] == { @@ -112,7 +125,9 @@ async def test_option_flow_default(hass): async def test_option_flow(hass): """Test config flow options.""" entry = MockConfigEntry( - domain=DOMAIN, data=CONFIG, options={CONF_UNIT_OF_MEASUREMENT: MG_DL}, + domain=DOMAIN, + data=CONFIG, + options={CONF_UNIT_OF_MEASUREMENT: MG_DL}, ) entry.add_to_hass(hass) @@ -122,7 +137,8 @@ async def test_option_flow(hass): assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, + result["flow_id"], + user_input={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { diff --git a/tests/components/dexcom/test_init.py b/tests/components/dexcom/test_init.py index 393aa9cfe98..2cb3ad3bf79 100644 --- a/tests/components/dexcom/test_init.py +++ b/tests/components/dexcom/test_init.py @@ -19,7 +19,8 @@ async def test_setup_entry_account_error(hass): options=None, ) with patch( - "homeassistant.components.dexcom.Dexcom", side_effect=AccountError, + "homeassistant.components.dexcom.Dexcom", + side_effect=AccountError, ): entry.add_to_hass(hass) result = await hass.config_entries.async_setup(entry.entry_id) @@ -38,7 +39,8 @@ async def test_setup_entry_session_error(hass): options=None, ) with patch( - "homeassistant.components.dexcom.Dexcom", side_effect=SessionError, + "homeassistant.components.dexcom.Dexcom", + side_effect=SessionError, ): entry.add_to_hass(hass) result = await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/dexcom/test_sensor.py b/tests/components/dexcom/test_sensor.py index de45e65155f..c9e00398140 100644 --- a/tests/components/dexcom/test_sensor.py +++ b/tests/components/dexcom/test_sensor.py @@ -98,7 +98,8 @@ async def test_sensors_options_changed(hass): return_value="test_session_id", ): hass.config_entries.async_update_entry( - entry=entry, options={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, + entry=entry, + options={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, ) await hass.async_block_till_done() diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 6df59873bd1..2213e52bef7 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -79,7 +79,8 @@ async def fixture(hass, aiohttp_client): ) await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 0082f9a5439..df3c606f687 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -28,7 +28,8 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_show_user_form(hass: HomeAssistantType) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, ) assert result["step_id"] == "user" @@ -59,7 +60,9 @@ async def test_cannot_connect( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_FORM @@ -75,7 +78,9 @@ async def test_ssdp_cannot_connect( discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -107,7 +112,9 @@ async def test_user_device_exists_abort( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_ABORT @@ -122,7 +129,9 @@ async def test_ssdp_device_exists_abort( discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -138,7 +147,9 @@ async def test_ssdp_with_receiver_id_device_exists_abort( discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() discovery_info[ATTR_UPNP_SERIAL] = UPNP_SERIAL result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -155,7 +166,9 @@ async def test_unknown_error( side_effect=Exception, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_ABORT @@ -172,7 +185,9 @@ async def test_ssdp_unknown_error( side_effect=Exception, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -209,7 +224,9 @@ async def test_full_import_flow_implementation( "homeassistant.components.directv.async_setup_entry", return_value=True ), patch("homeassistant.components.directv.async_setup", return_value=True): result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=user_input, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY @@ -227,7 +244,8 @@ async def test_full_user_flow_implementation( mock_connection(aioclient_mock) result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, ) assert result["type"] == RESULT_TYPE_FORM @@ -238,7 +256,8 @@ async def test_full_user_flow_implementation( "homeassistant.components.directv.async_setup_entry", return_value=True ), patch("homeassistant.components.directv.async_setup", return_value=True): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=user_input, + result["flow_id"], + user_input=user_input, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 7c7c034c6b4..f8a01899bd5 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -55,10 +55,12 @@ async def test_user_form(hass): ), patch( "homeassistant.components.doorbird.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", return_value=True, + "homeassistant.components.doorbird.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], VALID_CONFIG, + result["flow_id"], + VALID_CONFIG, ) assert result2["type"] == "create_entry" @@ -98,7 +100,8 @@ async def test_form_import(hass): ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( "homeassistant.components.doorbird.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", return_value=True, + "homeassistant.components.doorbird.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -165,7 +168,8 @@ async def test_form_import_with_zeroconf_already_discovered(hass): ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( "homeassistant.components.doorbird.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", return_value=True, + "homeassistant.components.doorbird.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -264,7 +268,8 @@ async def test_form_zeroconf_correct_oui(hass): ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( "homeassistant.components.doorbird.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", return_value=True, + "homeassistant.components.doorbird.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG @@ -299,7 +304,8 @@ async def test_form_user_cannot_connect(hass): return_value=doorbirdapi, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], VALID_CONFIG, + result["flow_id"], + VALID_CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -325,7 +331,8 @@ async def test_form_user_invalid_auth(hass): return_value=doorbirdapi, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], VALID_CONFIG, + result["flow_id"], + VALID_CONFIG, ) assert result2["type"] == "form" diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index 2336d2e4a3f..f2d30b2a8dd 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -39,7 +39,9 @@ async def test_import_cannot_connect(hass): async def test_import_duplicate_error(hass): """Test that errors are shown when duplicates are added during import.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "dunehd-host"}, title="dunehd-host", + domain=DOMAIN, + data={CONF_HOST: "dunehd-host"}, + title="dunehd-host", ) config_entry.add_to_hass(hass) @@ -74,7 +76,9 @@ async def test_user_cannot_connect(hass): async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" config_entry = MockConfigEntry( - domain=DOMAIN, data=CONFIG_HOSTNAME, title="dunehd-host", + domain=DOMAIN, + data=CONFIG_HOSTNAME, + title="dunehd-host", ) config_entry.add_to_hass(hass) diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index e36e6aeba2a..faa75aadef8 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -88,7 +88,8 @@ async def test_service_request_area_preset(hass): "homeassistant.components.dynalite.bridge.DynaliteDevices.async_setup", return_value=True, ), patch( - "dynalite_devices_lib.dynalite.Dynalite.request_area_preset", return_value=True, + "dynalite_devices_lib.dynalite.Dynalite.request_area_preset", + return_value=True, ) as mock_req_area_pres: assert await async_setup_component( hass, @@ -105,25 +106,33 @@ async def test_service_request_area_preset(hass): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(dynalite.DOMAIN)) == 2 await hass.services.async_call( - dynalite.DOMAIN, "request_area_preset", {"host": "1.2.3.4", "area": 2}, + dynalite.DOMAIN, + "request_area_preset", + {"host": "1.2.3.4", "area": 2}, ) await hass.async_block_till_done() mock_req_area_pres.assert_called_once_with(2, 1) mock_req_area_pres.reset_mock() await hass.services.async_call( - dynalite.DOMAIN, "request_area_preset", {"area": 3}, + dynalite.DOMAIN, + "request_area_preset", + {"area": 3}, ) await hass.async_block_till_done() assert mock_req_area_pres.mock_calls == [call(3, 1), call(3, 1)] mock_req_area_pres.reset_mock() await hass.services.async_call( - dynalite.DOMAIN, "request_area_preset", {"host": "5.6.7.8", "area": 4}, + dynalite.DOMAIN, + "request_area_preset", + {"host": "5.6.7.8", "area": 4}, ) await hass.async_block_till_done() mock_req_area_pres.assert_called_once_with(4, 1) mock_req_area_pres.reset_mock() await hass.services.async_call( - dynalite.DOMAIN, "request_area_preset", {"host": "6.5.4.3", "area": 5}, + dynalite.DOMAIN, + "request_area_preset", + {"host": "6.5.4.3", "area": 5}, ) await hass.async_block_till_done() mock_req_area_pres.assert_not_called() @@ -137,7 +146,9 @@ async def test_service_request_area_preset(hass): mock_req_area_pres.assert_called_once_with(6, 9) mock_req_area_pres.reset_mock() await hass.services.async_call( - dynalite.DOMAIN, "request_area_preset", {"host": "1.2.3.4", "area": 7}, + dynalite.DOMAIN, + "request_area_preset", + {"host": "1.2.3.4", "area": 7}, ) await hass.async_block_till_done() mock_req_area_pres.assert_called_once_with(7, 1) @@ -179,12 +190,16 @@ async def test_service_request_channel_level(hass): mock_req_chan_lvl.reset_mock() with pytest.raises(MultipleInvalid): await hass.services.async_call( - dynalite.DOMAIN, "request_channel_level", {"area": 3}, + dynalite.DOMAIN, + "request_channel_level", + {"area": 3}, ) await hass.async_block_till_done() mock_req_chan_lvl.assert_not_called() await hass.services.async_call( - dynalite.DOMAIN, "request_channel_level", {"area": 4, "channel": 5}, + dynalite.DOMAIN, + "request_channel_level", + {"area": 4, "channel": 5}, ) await hass.async_block_till_done() assert mock_req_chan_lvl.mock_calls == [call(4, 5), call(4, 5)] diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index 7d1d928cef8..1cf01bdb96c 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -35,14 +35,16 @@ def mock_responses(mock): """Mock responses for Efergy.""" base_url = "https://engage.efergy.com/mobile_proxy/" mock.get( - f"{base_url}getInstant?token={token}", text=load_fixture("efergy_instant.json"), + f"{base_url}getInstant?token={token}", + text=load_fixture("efergy_instant.json"), ) mock.get( f"{base_url}getEnergy?token={token}&offset=300&period=day", text=load_fixture("efergy_energy.json"), ) mock.get( - f"{base_url}getBudget?token={token}", text=load_fixture("efergy_budget.json"), + f"{base_url}getBudget?token={token}", + text=load_fixture("efergy_budget.json"), ) mock.get( f"{base_url}getCost?token={token}&offset=300&period=day", diff --git a/tests/components/elgato/__init__.py b/tests/components/elgato/__init__.py index 95791161a1f..5f0f2f5fb14 100644 --- a/tests/components/elgato/__init__.py +++ b/tests/components/elgato/__init__.py @@ -9,7 +9,9 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def init_integration( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + skip_setup: bool = False, ) -> MockConfigEntry: """Set up the Elgato Key Light integration in Home Assistant.""" diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index 7d50aec2e22..bd65811133a 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -17,7 +17,8 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + config_flow.DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" @@ -179,7 +180,8 @@ async def test_full_user_flow_implementation( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + config_flow.DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py index 13898dad757..838608c0aac 100644 --- a/tests/components/elgato/test_light.py +++ b/tests/components/elgato/test_light.py @@ -51,7 +51,8 @@ async def test_light_change_state( assert state.state == STATE_ON with patch( - "homeassistant.components.elgato.light.Elgato.light", return_value=mock_coro(), + "homeassistant.components.elgato.light.Elgato.light", + return_value=mock_coro(), ) as mock_light: await hass.services.async_call( LIGHT_DOMAIN, @@ -68,7 +69,8 @@ async def test_light_change_state( mock_light.assert_called_with(on=True, brightness=100, temperature=100) with patch( - "homeassistant.components.elgato.light.Elgato.light", return_value=mock_coro(), + "homeassistant.components.elgato.light.Elgato.light", + return_value=mock_coro(), ) as mock_light: await hass.services.async_call( LIGHT_DOMAIN, @@ -87,7 +89,8 @@ async def test_light_unavailable( """Test error/unavailable handling of an Elgato Key Light.""" await init_integration(hass, aioclient_mock) with patch( - "homeassistant.components.elgato.light.Elgato.light", side_effect=ElgatoError, + "homeassistant.components.elgato.light.Elgato.light", + side_effect=ElgatoError, ): with patch( "homeassistant.components.elgato.light.Elgato.state", diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 992483529a5..0a959cf1b97 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -26,11 +26,13 @@ async def test_form_user_with_secure_elk(hass): mocked_elk = mock_elk(invalid_auth=False) with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + "homeassistant.components.elkm1.config_flow.elkm1.Elk", + return_value=mocked_elk, ), patch( "homeassistant.components.elkm1.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", return_value=True, + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -71,11 +73,13 @@ async def test_form_user_with_non_secure_elk(hass): mocked_elk = mock_elk(invalid_auth=False) with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + "homeassistant.components.elkm1.config_flow.elkm1.Elk", + return_value=mocked_elk, ), patch( "homeassistant.components.elkm1.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", return_value=True, + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -114,11 +118,13 @@ async def test_form_user_with_serial_elk(hass): mocked_elk = mock_elk(invalid_auth=False) with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + "homeassistant.components.elkm1.config_flow.elkm1.Elk", + return_value=mocked_elk, ), patch( "homeassistant.components.elkm1.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", return_value=True, + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -154,7 +160,8 @@ async def test_form_cannot_connect(hass): mocked_elk = mock_elk(invalid_auth=False) with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + "homeassistant.components.elkm1.config_flow.elkm1.Elk", + return_value=mocked_elk, ), patch( "homeassistant.components.elkm1.config_flow.async_wait_for_elk_to_sync", return_value=False, @@ -184,7 +191,8 @@ async def test_form_invalid_auth(hass): mocked_elk = mock_elk(invalid_auth=True) with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + "homeassistant.components.elkm1.config_flow.elkm1.Elk", + return_value=mocked_elk, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -208,11 +216,13 @@ async def test_form_import(hass): mocked_elk = mock_elk(invalid_auth=False) with patch( - "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + "homeassistant.components.elkm1.config_flow.elkm1.Elk", + return_value=mocked_elk, ), patch( "homeassistant.components.elkm1.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.elkm1.async_setup_entry", return_value=True, + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index aee6765272e..b815d48694a 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -120,7 +120,8 @@ async def test_manual_flow_with_invalid_path(hass): USER_PROVIDED_PATH = "/user/provided/path" with patch( - DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=False), + DONGLE_VALIDATE_PATH_METHOD, + Mock(return_value=False), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} @@ -149,7 +150,8 @@ async def test_import_flow_with_invalid_path(hass): DATA_TO_IMPORT = {CONF_DEVICE: "/invalid/path/to/import"} with patch( - DONGLE_VALIDATE_PATH_METHOD, Mock(return_value=False), + DONGLE_VALIDATE_PATH_METHOD, + Mock(return_value=False), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data=DATA_TO_IMPORT diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 50aaa989028..4c5cb15a261 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -49,7 +49,9 @@ def mock_api_connection_error(): async def test_user_connection_works(hass, mock_client): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "esphome", context={"source": "user"}, data=None, + "esphome", + context={"source": "user"}, + data=None, ) assert result["type"] == RESULT_TYPE_FORM diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 84db16c1464..0aa390223ca 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -345,11 +345,16 @@ async def test_reload(hass): assert hass.states.get("sensor.test") yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "filter/configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "filter/configuration.yaml", ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, {}, blocking=True, + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index 90a6271aa7d..c8fe6298764 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -15,7 +15,9 @@ CONF = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} async def _flow_submit(hass): return await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONF, + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONF, ) @@ -34,10 +36,12 @@ async def test_form(hass): ), patch( "homeassistant.components.flick_electric.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.flick_electric.async_setup_entry", return_value=True, + "homeassistant.components.flick_electric.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONF, + result["flow_id"], + CONF, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 6f2c8d2ed04..bf95700c2de 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -31,14 +31,16 @@ async def test_form(hass): mock_flume_device_list = _get_mocked_flume_device_list() with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", return_value=True, + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, ), patch( "homeassistant.components.flume.config_flow.FlumeDeviceList", return_value=mock_flume_device_list, ), patch( "homeassistant.components.flume.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.flume.async_setup_entry", return_value=True, + "homeassistant.components.flume.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -69,14 +71,16 @@ async def test_form_import(hass): mock_flume_device_list = _get_mocked_flume_device_list() with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", return_value=True, + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, ), patch( "homeassistant.components.flume.config_flow.FlumeDeviceList", return_value=mock_flume_device_list, ), patch( "homeassistant.components.flume.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.flume.async_setup_entry", return_value=True, + "homeassistant.components.flume.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -109,7 +113,8 @@ async def test_form_invalid_auth(hass): ) with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", return_value=True, + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, ), patch( "homeassistant.components.flume.config_flow.FlumeDeviceList", side_effect=Exception, @@ -134,7 +139,8 @@ async def test_form_cannot_connect(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.flume.config_flow.FlumeAuth", return_value=True, + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, ), patch( "homeassistant.components.flume.config_flow.FlumeDeviceList", side_effect=requests.exceptions.ConnectionError(), diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py index a3a0d41e885..d4e650e2b4a 100644 --- a/tests/components/flunearyou/test_config_flow.py +++ b/tests/components/flunearyou/test_config_flow.py @@ -31,7 +31,8 @@ async def test_general_error(hass): conf = {CONF_LATITUDE: "51.528308", CONF_LONGITUDE: "-0.3817765"} with patch( - "pyflunearyou.cdc.CdcReport.status_by_coordinates", side_effect=FluNearYouError, + "pyflunearyou.cdc.CdcReport.status_by_coordinates", + side_effect=FluNearYouError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index 17b30121aaf..181c963ee4f 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -89,7 +89,9 @@ async def test_config_flow(hass, config_entry): # Also test that creating a new entry with the same host aborts result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config_entry.data, + DOMAIN, + context={"source": SOURCE_USER}, + data=config_entry.data, ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 4f5cb57d66c..15755949062 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -528,7 +528,9 @@ async def test_bunch_of_stuff_master(hass, get_request_return_values, mock_api_o await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TOGGLE) for output in SAMPLE_OUTPUTS_ON: mock_api_object.change_output.assert_any_call( - output["id"], selected=output["selected"], volume=output["volume"], + output["id"], + selected=output["selected"], + volume=output["volume"], ) mock_api_object.set_volume.assert_any_call(volume=0) mock_api_object.set_volume.assert_any_call(volume=SAMPLE_PLAYER_PAUSED["volume"]) diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index 366573a276c..93b24269441 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -23,7 +23,9 @@ MOCK_CONF = { @pytest.fixture(name="mock_garmin_connect") def mock_garmin(): """Mock Garmin.""" - with patch("homeassistant.components.garmin_connect.config_flow.Garmin",) as garmin: + with patch( + "homeassistant.components.garmin_connect.config_flow.Garmin", + ) as garmin: garmin.return_value.get_full_name.return_value = MOCK_CONF[CONF_ID] yield garmin.return_value diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index 9be6742deed..b123021a7e3 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -29,10 +29,24 @@ async def test_setup(hass, legacy_patchable_time): """Test the general setup of the integration.""" # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry( - "1234", "Title 1", 15.5, (38.0, -3.0), attribution="Attribution 1", + "1234", + "Title 1", + 15.5, + (38.0, -3.0), + attribution="Attribution 1", + ) + mock_entry_2 = _generate_mock_feed_entry( + "2345", + "Title 2", + 20.5, + (38.1, -3.1), + ) + mock_entry_3 = _generate_mock_feed_entry( + "3456", + "Title 3", + 25.5, + (38.2, -3.2), ) - mock_entry_2 = _generate_mock_feed_entry("2345", "Title 2", 20.5, (38.1, -3.1),) - mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 25.5, (38.2, -3.2),) mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (38.3, -3.3)) # Patching 'utcnow' to gain more control over the timed update. diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 3a9bdf1fe73..dd3132b0117 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -150,7 +150,8 @@ async def setup_zones(loop, hass): async def webhook_id(hass, geofency_client): """Initialize the Geofency component and get the webhook_id.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} diff --git a/tests/components/gogogate2/common.py b/tests/components/gogogate2/common.py index d344a31cf4b..84999cb0e29 100644 --- a/tests/components/gogogate2/common.py +++ b/tests/components/gogogate2/common.py @@ -129,7 +129,8 @@ class ComponentFactory: assert result["step_id"] == "user" result = await self._hass.config_entries.flow.async_configure( - result["flow_id"], user_input=config_data, + result["flow_id"], + user_input=config_data, ) assert result assert result["type"] == RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index e921df406d2..55de8701b61 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -22,7 +22,8 @@ async def test_auth_fail( with patch( "homeassistant.components.gogogate2.async_setup", return_value=True ), patch( - "homeassistant.components.gogogate2.async_setup_entry", return_value=True, + "homeassistant.components.gogogate2.async_setup_entry", + return_value=True, ): await component_factory.configure_component() component_factory.api_class_mock.return_value = api_mock diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 5bc9ed9ebd4..b754b88c05e 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -398,7 +398,9 @@ async def test_open_close( assert hass.states.get("cover.door1").state == STATE_OPEN await hass.services.async_call( - COVER_DOMAIN, "close_cover", service_data={"entity_id": "cover.door1"}, + COVER_DOMAIN, + "close_cover", + service_data={"entity_id": "cover.door1"}, ) await hass.async_block_till_done() component_data.api.close_door.assert_called_with(1) @@ -410,7 +412,9 @@ async def test_open_close( # Assert mid state changed when new status is received. await hass.services.async_call( - COVER_DOMAIN, "open_cover", service_data={"entity_id": "cover.door1"}, + COVER_DOMAIN, + "open_cover", + service_data={"entity_id": "cover.door1"}, ) await hass.async_block_till_done() component_data.api.open_door.assert_called_with(1) @@ -423,7 +427,9 @@ async def test_open_close( await component_data.data_update_coordinator.async_refresh() await hass.services.async_call( - HA_DOMAIN, "update_entity", service_data={"entity_id": "cover.door1"}, + HA_DOMAIN, + "update_entity", + service_data={"entity_id": "cover.door1"}, ) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index c58f44c06c8..2bd68926da1 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -27,7 +27,8 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): hass.states.async_set("light.ceiling_lights", "off") hass.config.api = Mock(port=1234, use_ssl=True) await async_process_ha_core_config( - hass, {"external_url": "https://hostname:1234"}, + hass, + {"external_url": "https://hostname:1234"}, ) hass.http = Mock(server_port=1234) @@ -227,7 +228,8 @@ async def test_report_state_all(agents): @pytest.mark.parametrize( - "agents, result", [({}, 204), ({"1": 200}, 200), ({"1": 200, "2": 300}, 300)], + "agents, result", + [({}, 204), ({"1": 200}, 200), ({"1": 200, "2": 300}, 300)], ) async def test_sync_entities_all(agents, result): """Test sync entities .""" diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 0df2b032b5a..e663df19d88 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -17,7 +17,9 @@ async def test_request_sync_service(aioclient_mock, hass): aioclient_mock.post(ga.const.REQUEST_SYNC_BASE_URL, status=200) await async_setup_component( - hass, "google_assistant", {"google_assistant": DUMMY_CONFIG}, + hass, + "google_assistant", + {"google_assistant": DUMMY_CONFIG}, ) assert aioclient_mock.call_count == 0 diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index db97a42dff4..f3bd42c1181 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -914,7 +914,8 @@ async def test_query_disconnect(hass): async def test_trait_execute_adding_query_data(hass): """Test a trait execute influencing query data.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) hass.states.async_set( "camera.office", "idle", {"supported_features": camera.SUPPORT_STREAM} diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a8f6b58fc46..ae51ba76ffc 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -110,7 +110,8 @@ async def test_brightness_light(hass): async def test_camera_stream(hass): """Test camera stream trait support for camera domain.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) assert helpers.get_google_type(camera.DOMAIN, None) is not None assert trait.CameraStreamTrait.supported(camera.DOMAIN, camera.SUPPORT_STREAM, None) @@ -564,7 +565,8 @@ async def test_light_modes(hass): } assert trt.can_execute( - trait.COMMAND_MODES, params={"updateModeSettings": {"effect": "colorloop"}}, + trait.COMMAND_MODES, + params={"updateModeSettings": {"effect": "colorloop"}}, ) calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) @@ -988,7 +990,9 @@ async def test_lock_unlock_unlock(hass): # Test with 2FA override with patch.object( - BASIC_CONFIG, "should_2fa", return_value=False, + BASIC_CONFIG, + "should_2fa", + return_value=False, ): await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {"lock": False}, {}) assert len(calls) == 2 @@ -1150,7 +1154,10 @@ async def test_arm_disarm_arm_away(hass): with pytest.raises(error.SmartHomeError) as err: await trt.execute( - trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": True}, {}, + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True}, + {}, ) @@ -1483,13 +1490,19 @@ async def test_inputselector(hass): "currentInput": "game", } - assert trt.can_execute(trait.COMMAND_INPUT, params={"newInput": "media"},) + assert trt.can_execute( + trait.COMMAND_INPUT, + params={"newInput": "media"}, + ) calls = async_mock_service( hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE ) await trt.execute( - trait.COMMAND_INPUT, BASIC_DATA, {"newInput": "media"}, {}, + trait.COMMAND_INPUT, + BASIC_DATA, + {"newInput": "media"}, + {}, ) assert len(calls) == 1 @@ -1526,10 +1539,16 @@ async def test_inputselector_nextprev(hass, sources, source, source_next, source hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE ) await trt.execute( - "action.devices.commands.NextInput", BASIC_DATA, {}, {}, + "action.devices.commands.NextInput", + BASIC_DATA, + {}, + {}, ) await trt.execute( - "action.devices.commands.PreviousInput", BASIC_DATA, {}, {}, + "action.devices.commands.PreviousInput", + BASIC_DATA, + {}, + {}, ) assert len(calls) == 2 @@ -1563,17 +1582,26 @@ async def test_inputselector_nextprev_invalid(hass, sources, source): with pytest.raises(SmartHomeError): await trt.execute( - "action.devices.commands.NextInput", BASIC_DATA, {}, {}, + "action.devices.commands.NextInput", + BASIC_DATA, + {}, + {}, ) with pytest.raises(SmartHomeError): await trt.execute( - "action.devices.commands.PreviousInput", BASIC_DATA, {}, {}, + "action.devices.commands.PreviousInput", + BASIC_DATA, + {}, + {}, ) with pytest.raises(SmartHomeError): await trt.execute( - "action.devices.commands.InvalidCommand", BASIC_DATA, {}, {}, + "action.devices.commands.InvalidCommand", + BASIC_DATA, + {}, + {}, ) @@ -1583,7 +1611,9 @@ async def test_modes_input_select(hass): assert trait.ModesTrait.supported(input_select.DOMAIN, None, None) trt = trait.ModesTrait( - hass, State("input_select.bla", "unavailable"), BASIC_CONFIG, + hass, + State("input_select.bla", "unavailable"), + BASIC_CONFIG, ) assert trt.sync_attributes() == {"availableModes": []} @@ -1633,14 +1663,18 @@ async def test_modes_input_select(hass): } assert trt.can_execute( - trait.COMMAND_MODES, params={"updateModeSettings": {"option": "xyz"}}, + trait.COMMAND_MODES, + params={"updateModeSettings": {"option": "xyz"}}, ) calls = async_mock_service( hass, input_select.DOMAIN, input_select.SERVICE_SELECT_OPTION ) await trt.execute( - trait.COMMAND_MODES, BASIC_DATA, {"updateModeSettings": {"option": "xyz"}}, {}, + trait.COMMAND_MODES, + BASIC_DATA, + {"updateModeSettings": {"option": "xyz"}}, + {}, ) assert len(calls) == 1 @@ -1711,7 +1745,10 @@ async def test_modes_humidifier(hass): calls = async_mock_service(hass, humidifier.DOMAIN, humidifier.SERVICE_SET_MODE) await trt.execute( - trait.COMMAND_MODES, BASIC_DATA, {"updateModeSettings": {"mode": "away"}}, {}, + trait.COMMAND_MODES, + BASIC_DATA, + {"updateModeSettings": {"mode": "away"}}, + {}, ) assert len(calls) == 1 @@ -1774,7 +1811,8 @@ async def test_sound_modes(hass): } assert trt.can_execute( - trait.COMMAND_MODES, params={"updateModeSettings": {"sound mode": "stereo"}}, + trait.COMMAND_MODES, + params={"updateModeSettings": {"sound mode": "stereo"}}, ) calls = async_mock_service( @@ -2003,7 +2041,9 @@ async def test_volume_media_player(hass): """Test volume trait support for media player domain.""" assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.VolumeTrait.supported( - media_player.DOMAIN, media_player.SUPPORT_VOLUME_SET, None, + media_player.DOMAIN, + media_player.SUPPORT_VOLUME_SET, + None, ) trt = trait.VolumeTrait( @@ -2054,7 +2094,9 @@ async def test_volume_media_player(hass): async def test_volume_media_player_relative(hass): """Test volume trait support for relative-volume-only media players.""" assert trait.VolumeTrait.supported( - media_player.DOMAIN, media_player.SUPPORT_VOLUME_STEP, None, + media_player.DOMAIN, + media_player.SUPPORT_VOLUME_STEP, + None, ) trt = trait.VolumeTrait( hass, @@ -2083,7 +2125,10 @@ async def test_volume_media_player_relative(hass): ) await trt.execute( - trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"relativeSteps": 10}, {}, + trait.COMMAND_VOLUME_RELATIVE, + BASIC_DATA, + {"relativeSteps": 10}, + {}, ) assert len(calls) == 10 for call in calls: @@ -2095,7 +2140,10 @@ async def test_volume_media_player_relative(hass): hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_DOWN ) await trt.execute( - trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"relativeSteps": -10}, {}, + trait.COMMAND_VOLUME_RELATIVE, + BASIC_DATA, + {"relativeSteps": -10}, + {}, ) assert len(calls) == 10 for call in calls: @@ -2144,7 +2192,10 @@ async def test_media_player_mute(hass): hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_MUTE ) await trt.execute( - trait.COMMAND_MUTE, BASIC_DATA, {"mute": True}, {}, + trait.COMMAND_MUTE, + BASIC_DATA, + {"mute": True}, + {}, ) assert len(mute_calls) == 1 assert mute_calls[0].data == { @@ -2156,7 +2207,10 @@ async def test_media_player_mute(hass): hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_MUTE ) await trt.execute( - trait.COMMAND_MUTE, BASIC_DATA, {"mute": False}, {}, + trait.COMMAND_MUTE, + BASIC_DATA, + {"mute": False}, + {}, ) assert len(unmute_calls) == 1 assert unmute_calls[0].data == { diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 18e80647fe7..39dc05303b3 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -64,7 +64,8 @@ async def setup_zones(loop, hass): async def webhook_id(hass, gpslogger_client): """Initialize the GPSLogger component and get the webhook_id.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} diff --git a/tests/components/griddy/test_config_flow.py b/tests/components/griddy/test_config_flow.py index 79f99d7f8b1..309864dbc11 100644 --- a/tests/components/griddy/test_config_flow.py +++ b/tests/components/griddy/test_config_flow.py @@ -22,10 +22,12 @@ async def test_form(hass): ), patch( "homeassistant.components.griddy.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.griddy.async_setup_entry", return_value=True, + "homeassistant.components.griddy.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"loadzone": "LZ_HOUSTON"}, + result["flow_id"], + {"loadzone": "LZ_HOUSTON"}, ) assert result2["type"] == "create_entry" @@ -47,7 +49,8 @@ async def test_form_cannot_connect(hass): side_effect=asyncio.TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"loadzone": "LZ_NORTH"}, + result["flow_id"], + {"loadzone": "LZ_NORTH"}, ) assert result2["type"] == "form" diff --git a/tests/components/group/common.py b/tests/components/group/common.py index 69de1cfee75..b2c35703e6c 100644 --- a/tests/components/group/common.py +++ b/tests/components/group/common.py @@ -31,18 +31,34 @@ def async_reload(hass): @bind_hass def set_group( - hass, object_id, name=None, entity_ids=None, icon=None, add=None, + hass, + object_id, + name=None, + entity_ids=None, + icon=None, + add=None, ): """Create/Update a group.""" hass.add_job( - async_set_group, hass, object_id, name, entity_ids, icon, add, + async_set_group, + hass, + object_id, + name, + entity_ids, + icon, + add, ) @callback @bind_hass def async_set_group( - hass, object_id, name=None, entity_ids=None, icon=None, add=None, + hass, + object_id, + name=None, + entity_ids=None, + icon=None, + add=None, ): """Create/Update a group.""" data = { diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 5ca008ba91f..dae975ead03 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -667,11 +667,16 @@ async def test_reload(hass): assert hass.states.get("light.light_group").state == STATE_ON yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "group/configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "group/configuration.yaml", ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, {}, blocking=True, + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index 91a1a3b83e0..a10bee374f8 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -35,7 +35,8 @@ async def test_connect_error(hass): conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PORT: 7777} with patch( - "aioguardian.client.Client.connect", side_effect=GuardianError, + "aioguardian.client.Client.connect", + side_effect=GuardianError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index acf1ffd16f1..d6bd4022d9e 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -44,14 +44,17 @@ async def test_user_form(hass): harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", + return_value=harmonyapi, ), patch( "homeassistant.components.harmony.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.harmony.async_setup_entry", return_value=True, + "homeassistant.components.harmony.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.2.3.4", "name": "friend"}, + result["flow_id"], + {"host": "1.2.3.4", "name": "friend"}, ) assert result2["type"] == "create_entry" @@ -68,11 +71,13 @@ async def test_form_import(hass): harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", + return_value=harmonyapi, ), patch( "homeassistant.components.harmony.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.harmony.async_setup_entry", return_value=True, + "homeassistant.components.harmony.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -110,7 +115,8 @@ async def test_form_ssdp(hass): harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", + return_value=harmonyapi, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -129,13 +135,18 @@ async def test_form_ssdp(hass): } with patch( - "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", + return_value=harmonyapi, ), patch( "homeassistant.components.harmony.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.harmony.async_setup_entry", return_value=True, + "homeassistant.components.harmony.async_setup_entry", + return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result2["type"] == "create_entry" assert result2["title"] == "Harmony Hub" @@ -149,17 +160,22 @@ async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass): """Test we abort without connecting if the host is already known.""" await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( - domain=DOMAIN, data={"host": "2.2.2.2", "name": "any"}, + domain=DOMAIN, + data={"host": "2.2.2.2", "name": "any"}, ) config_entry.add_to_hass(hass) - config_entry_without_host = MockConfigEntry(domain=DOMAIN, data={"name": "other"},) + config_entry_without_host = MockConfigEntry( + domain=DOMAIN, + data={"name": "other"}, + ) config_entry_without_host.add_to_hass(hass) harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", + return_value=harmonyapi, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -179,7 +195,8 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.harmony.util.HarmonyAPI", side_effect=CannotConnect, + "homeassistant.components.harmony.util.HarmonyAPI", + side_effect=CannotConnect, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -208,7 +225,8 @@ async def test_options_flow(hass): harmony_client = _get_mock_harmonyclient() with patch( - "aioharmony.harmonyapi.HarmonyClient", return_value=harmony_client, + "aioharmony.harmonyapi.HarmonyClient", + return_value=harmony_client, ), patch("homeassistant.components.harmony.remote.HarmonyRemote.write_config_file"): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 5768d192c8a..f52a825ca29 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -35,7 +35,8 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): "homeassistant.components.hassio.HassIO.update_hass_timezone", return_value={"result": "ok"}, ), patch( - "homeassistant.components.hassio.HassIO.get_info", side_effect=HassioAPIError(), + "homeassistant.components.hassio.HassIO.get_info", + side_effect=HassioAPIError(), ): hass.state = CoreState.starting hass.loop.run_until_complete(async_setup_component(hass, "hassio", {})) @@ -60,7 +61,8 @@ async def hassio_client_supervisor(hass, aiohttp_client, hassio_stubs): """Return an authenticated HTTP client.""" access_token = hass.auth.async_create_access_token(hassio_stubs) return await aiohttp_client( - hass.http.app, headers={"Authorization": f"Bearer {access_token}"}, + hass.http.app, + headers={"Authorization": f"Bearer {access_token}"}, ) diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 1cfb5ed9f7b..3bb97a6662e 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -61,7 +61,8 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client): """Test startup and discovery with hass discovery.""" aioclient_mock.post( - "http://127.0.0.1/supervisor/options", json={"result": "ok", "data": {}}, + "http://127.0.0.1/supervisor/options", + json={"result": "ok", "data": {}}, ) aioclient_mock.get( "http://127.0.0.1/discovery", diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 34ba638410a..56792295fec 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -213,7 +213,8 @@ async def test_fail_setup_without_environ_var(hass): async def test_warn_when_cannot_connect(hass, caplog): """Fail warn when we cannot connect.""" with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.hassio.HassIO.is_connected", return_value=None, + "homeassistant.components.hassio.HassIO.is_connected", + return_value=None, ): result = await async_setup_component(hass, "hassio", {}) assert result diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index 498e8c4c306..bf3c9d2c6a4 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -78,7 +78,8 @@ async def test_configuring_hisense_w4a1_not_creates_entry_for_device_not_found(h async def test_configuring_hisense_w4a1_not_creates_entry_for_empty_import(hass): """Test that specifying config will not create an entry.""" with patch( - "homeassistant.components.hisense_aehw4a1.async_setup_entry", return_value=True, + "homeassistant.components.hisense_aehw4a1.async_setup_entry", + return_value=True, ) as mock_setup: await async_setup_component(hass, hisense_aehw4a1.DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index e9544ce9035..c15e4431f87 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -257,7 +257,8 @@ class TestComponentHistory(unittest.TestCase): # will happen with encoding a native state input_state = states["media_player.test"][1] orig_last_changed = json.dumps( - process_timestamp(input_state.last_changed), cls=JSONEncoder, + process_timestamp(input_state.last_changed), + cls=JSONEncoder, ).replace('"', "") orig_state = input_state.state states["media_player.test"][1] = { diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index 6f9d5592893..7b7468047be 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -70,10 +70,12 @@ async def test_form(hass): ), patch( "homeassistant.components.hlk_sw16.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.hlk_sw16.async_setup_entry", return_value=True, + "homeassistant.components.hlk_sw16.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], conf, + result["flow_id"], + conf, ) assert result2["type"] == "create_entry" @@ -98,7 +100,10 @@ async def test_form(hass): assert result3["type"] == "form" assert result3["errors"] == {} - result4 = await hass.config_entries.flow.async_configure(result3["flow_id"], conf,) + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + conf, + ) assert result4["type"] == "form" assert result4["errors"] == {"base": "already_configured"} @@ -127,10 +132,12 @@ async def test_import(hass): ), patch( "homeassistant.components.hlk_sw16.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.hlk_sw16.async_setup_entry", return_value=True, + "homeassistant.components.hlk_sw16.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], conf, + result["flow_id"], + conf, ) assert result2["type"] == "create_entry" @@ -162,7 +169,8 @@ async def test_form_invalid_data(hass): return_value=mock_hlk_sw16_connection, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], conf, + result["flow_id"], + conf, ) assert result2["type"] == "form" @@ -186,7 +194,8 @@ async def test_form_cannot_connect(hass): return_value=None, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], conf, + result["flow_id"], + conf, ) assert result2["type"] == "form" diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 3419aea06af..b2ca49712d1 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -266,7 +266,8 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass): service = hass.services._services["homeassistant"]["turn_on"] with patch( - "homeassistant.core.ServiceRegistry.async_call", return_value=None, + "homeassistant.core.ServiceRegistry.async_call", + return_value=None, ) as mock_call: await service.func(service_call) @@ -290,7 +291,8 @@ async def test_entity_update(hass): await async_setup_component(hass, "homeassistant", {}) with patch( - "homeassistant.helpers.entity_component.async_update_entity", return_value=None, + "homeassistant.helpers.entity_component.async_update_entity", + return_value=None, ) as mock_update: await hass.services.async_call( "homeassistant", @@ -373,7 +375,10 @@ async def test_not_allowing_recursion(hass, caplog): for service in SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE: await hass.services.async_call( - ha.DOMAIN, service, {"entity_id": "homeassistant.light"}, blocking=True, + ha.DOMAIN, + service, + {"entity_id": "homeassistant.light"}, + blocking=True, ) assert ( f"Called service homeassistant.{service} with invalid entity IDs homeassistant.light" diff --git a/tests/components/homeassistant/triggers/test_homeassistant.py b/tests/components/homeassistant/triggers/test_homeassistant.py index d7bdfbeef3e..9272c3620af 100644 --- a/tests/components/homeassistant/triggers/test_homeassistant.py +++ b/tests/components/homeassistant/triggers/test_homeassistant.py @@ -29,7 +29,8 @@ async def test_if_fires_on_hass_start(hass): assert len(calls) == 1 with patch( - "homeassistant.config.async_hass_config_yaml", AsyncMock(return_value=config), + "homeassistant.config.async_hass_config_yaml", + AsyncMock(return_value=config), ): await hass.services.async_call( automation.DOMAIN, automation.SERVICE_RELOAD, blocking=True diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index fd3d546a507..6e4e03f2137 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -45,7 +45,8 @@ async def test_user_form(hass): return_value=12345, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"auto_start": True, "include_domains": ["light"]}, + result["flow_id"], + {"auto_start": True, "include_domains": ["light"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -54,9 +55,13 @@ async def test_user_form(hass): with patch( "homeassistant.components.homekit.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.homekit.async_setup_entry", return_value=True, + "homeassistant.components.homekit.async_setup_entry", + return_value=True, ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" @@ -98,7 +103,8 @@ async def test_import(hass): with patch( "homeassistant.components.homekit.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.homekit.async_setup_entry", return_value=True, + "homeassistant.components.homekit.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_init( DOMAIN, @@ -142,14 +148,16 @@ async def test_options_flow_advanced(hass): assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"exclude_entities": ["climate.old"]}, + result["flow_id"], + user_input={"exclude_entities": ["climate.old"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "advanced" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], user_input={"auto_start": True, "safe_mode": True}, + result2["flow_id"], + user_input={"auto_start": True, "safe_mode": True}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -182,21 +190,24 @@ async def test_options_flow_basic(hass): assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"include_domains": ["fan", "vacuum", "climate"]}, + result["flow_id"], + user_input={"include_domains": ["fan", "vacuum", "climate"]}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"exclude_entities": ["climate.old"]}, + result["flow_id"], + user_input={"exclude_entities": ["climate.old"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "advanced" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], user_input={"safe_mode": True}, + result2["flow_id"], + user_input={"safe_mode": True}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -248,7 +259,8 @@ async def test_options_flow_with_cameras(hass): assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], user_input={"camera_copy": ["camera.native_h264"]}, + result2["flow_id"], + user_input={"camera_copy": ["camera.native_h264"]}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -256,7 +268,8 @@ async def test_options_flow_with_cameras(hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], user_input={"safe_mode": True}, + result3["flow_id"], + user_input={"safe_mode": True}, ) assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -297,7 +310,8 @@ async def test_options_flow_with_cameras(hass): assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], user_input={"camera_copy": []}, + result2["flow_id"], + user_input={"camera_copy": []}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -305,7 +319,8 @@ async def test_options_flow_with_cameras(hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], user_input={"safe_mode": True}, + result3["flow_id"], + user_input={"safe_mode": True}, ) assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -356,6 +371,7 @@ async def test_options_flow_blocked_when_from_yaml(hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result2 = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={}, + result["flow_id"], + user_input={}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 85f517f28be..9b687e5dda6 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -271,7 +271,8 @@ def test_type_vacuum(type_name, entity_id, state, attrs): @pytest.mark.parametrize( - "type_name, entity_id, state, attrs", [("Camera", "camera.basic", "on", {})], + "type_name, entity_id, state, attrs", + [("Camera", "camera.basic", "on", {})], ) def test_type_camera(type_name, entity_id, state, attrs): """Test if camera types are associated correctly.""" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 8e8e9a3506f..c20b2a9d9fb 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -917,7 +917,8 @@ async def test_raise_config_entry_not_ready(hass): entry.add_to_hass(hass) with patch( - "homeassistant.components.homekit.port_is_available", return_value=False, + "homeassistant.components.homekit.port_is_available", + return_value=False, ): assert not await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 118ce2d9934..6386b1d8e69 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -142,7 +142,14 @@ async def test_camera_stream_source_configured(hass, run_driver, events): 2, {CONF_STREAM_SOURCE: "/dev/null", CONF_SUPPORT_AUDIO: True}, ) - not_camera_acc = Switch(hass, run_driver, "Switch", entity_id, 4, {},) + not_camera_acc = Switch( + hass, + run_driver, + "Switch", + entity_id, + 4, + {}, + ) bridge = HomeBridge("hass", run_driver, "Test Bridge") bridge.add_accessory(acc) bridge.add_accessory(not_camera_acc) @@ -247,7 +254,14 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg( 2, {CONF_STREAM_SOURCE: "/dev/null", CONF_SUPPORT_AUDIO: True}, ) - not_camera_acc = Switch(hass, run_driver, "Switch", entity_id, 4, {},) + not_camera_acc = Switch( + hass, + run_driver, + "Switch", + entity_id, + 4, + {}, + ) bridge = HomeBridge("hass", run_driver, "Test Bridge") bridge.add_accessory(acc) bridge.add_accessory(not_camera_acc) @@ -284,7 +298,14 @@ async def test_camera_stream_source_found(hass, run_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Camera(hass, run_driver, "Camera", entity_id, 2, {},) + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + {}, + ) await acc.run_handler() assert acc.aid == 2 @@ -327,7 +348,14 @@ async def test_camera_stream_source_fails(hass, run_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Camera(hass, run_driver, "Camera", entity_id, 2, {},) + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + {}, + ) await acc.run_handler() assert acc.aid == 2 @@ -355,7 +383,14 @@ async def test_camera_with_no_stream(hass, run_driver, events): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Camera(hass, run_driver, "Camera", entity_id, 2, {},) + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + {}, + ) await acc.run_handler() assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 806aa60ee50..3eed6d05816 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -437,7 +437,10 @@ async def test_window_basic_restore(hass, hk_driver, cls, events): registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( - "cover", "generic", "1234", suggested_object_id="simple", + "cover", + "generic", + "1234", + suggested_object_id="simple", ) registry.async_get_or_create( "cover", @@ -472,7 +475,10 @@ async def test_window_restore(hass, hk_driver, cls, events): registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( - "cover", "generic", "1234", suggested_object_id="simple", + "cover", + "generic", + "1234", + suggested_object_id="simple", ) registry.async_get_or_create( "cover", diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 6d5b0f9841b..a25567b7004 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -566,7 +566,10 @@ async def test_fan_restore(hass, hk_driver, cls, events): registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( - "fan", "generic", "1234", suggested_object_id="simple", + "fan", + "generic", + "1234", + suggested_object_id="simple", ) registry.async_get_or_create( "fan", diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py index 34f61a5c0df..f9ac65a1942 100644 --- a/tests/components/homekit/test_type_humidifiers.py +++ b/tests/components/homekit/test_type_humidifiers.py @@ -74,7 +74,9 @@ async def test_humidifier(hass, hk_driver, events): } hass.states.async_set( - entity_id, STATE_ON, {ATTR_HUMIDITY: 47}, + entity_id, + STATE_ON, + {ATTR_HUMIDITY: 47}, ) await hass.async_block_till_done() assert acc.char_target_humidity.value == 47.0 @@ -153,7 +155,9 @@ async def test_dehumidifier(hass, hk_driver, events): } hass.states.async_set( - entity_id, STATE_ON, {ATTR_HUMIDITY: 30}, + entity_id, + STATE_ON, + {ATTR_HUMIDITY: 30}, ) await hass.async_block_till_done() assert acc.char_target_humidity.value == 30.0 @@ -162,7 +166,9 @@ async def test_dehumidifier(hass, hk_driver, events): assert acc.char_active.value == 1 hass.states.async_set( - entity_id, STATE_OFF, {ATTR_HUMIDITY: 42}, + entity_id, + STATE_OFF, + {ATTR_HUMIDITY: 42}, ) await hass.async_block_till_done() assert acc.char_target_humidity.value == 42.0 @@ -204,7 +210,9 @@ async def test_hygrostat_power_state(hass, hk_driver, events): entity_id = "humidifier.test" hass.states.async_set( - entity_id, STATE_ON, {ATTR_HUMIDITY: 43}, + entity_id, + STATE_ON, + {ATTR_HUMIDITY: 43}, ) await hass.async_block_till_done() acc = HumidifierDehumidifier( @@ -220,7 +228,9 @@ async def test_hygrostat_power_state(hass, hk_driver, events): assert acc.char_active.value == 1 hass.states.async_set( - entity_id, STATE_OFF, {ATTR_HUMIDITY: 43}, + entity_id, + STATE_OFF, + {ATTR_HUMIDITY: 43}, ) await hass.async_block_till_done() assert acc.char_current_humidifier_dehumidifier.value == 0 @@ -397,9 +407,9 @@ async def test_humidifier_as_dehumidifier(hass, hk_driver, events, caplog): assert acc.char_target_humidifier_dehumidifier.value == 1 # Set from HomeKit - char_target_humidifier_dehumidifier_iid = acc.char_target_humidifier_dehumidifier.to_HAP()[ - HAP_REPR_IID - ] + char_target_humidifier_dehumidifier_iid = ( + acc.char_target_humidifier_dehumidifier.to_HAP()[HAP_REPR_IID] + ) hk_driver.set_characteristics( { diff --git a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py index e9fc9b522ea..fbdf00698f7 100644 --- a/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_homeassistant_bridge.py @@ -28,7 +28,11 @@ async def test_homeassistant_bridge_fan_setup(hass): assert fan.unique_id == "homekit-fan.living_room_fan-8" fan_helper = Helper( - hass, "fan.living_room_fan", pairing, accessories[0], config_entry, + hass, + "fan.living_room_fan", + pairing, + accessories[0], + config_entry, ) fan_state = await fan_helper.poll_and_get_state() diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py index 9e88d9d82e9..a83a31166f4 100644 --- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -26,7 +26,11 @@ async def test_simpleconnect_fan_setup(hass): assert fan.unique_id == "homekit-1234567890abcd-8" fan_helper = Helper( - hass, "fan.simpleconnect_fan_06f674", pairing, accessories[0], config_entry, + hass, + "fan.simpleconnect_fan_06f674", + pairing, + accessories[0], + config_entry, ) fan_state = await fan_helper.poll_and_get_state() diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index fd24f5215da..e9ebce4045b 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -102,7 +102,10 @@ async def test_turn_off(hass, utcnow): helper.characteristics[V1_ON].value = 1 await hass.services.async_call( - "fan", "turn_off", {"entity_id": "fan.testdevice"}, blocking=True, + "fan", + "turn_off", + {"entity_id": "fan.testdevice"}, + blocking=True, ) assert helper.characteristics[V1_ON].value == 0 @@ -255,7 +258,10 @@ async def test_v2_turn_off(hass, utcnow): helper.characteristics[V2_ACTIVE].value = 1 await hass.services.async_call( - "fan", "turn_off", {"entity_id": "fan.testdevice"}, blocking=True, + "fan", + "turn_off", + {"entity_id": "fan.testdevice"}, + blocking=True, ) assert helper.characteristics[V2_ACTIVE].value == 0 diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index cfde3413916..6be0036a737 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -31,7 +31,9 @@ from .helper import async_manipulate_test_data, get_and_check_entity_basics async def test_manually_configured_platform(hass): """Test that we do not set up an access point.""" assert await async_setup_component( - hass, BINARY_SENSOR_DOMAIN, {BINARY_SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}}, + hass, + BINARY_SENSOR_DOMAIN, + {BINARY_SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}}, ) assert not hass.data.get(HMIPC_DOMAIN) @@ -245,7 +247,10 @@ async def test_hmip_smoke_detector(hass, default_mock_hap_factory): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON await async_manipulate_test_data( - hass, hmip_device, "smokeDetectorAlarmType", None, + hass, + hmip_device, + "smokeDetectorAlarmType", + None, ) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 52ca13aad62..6d5c3fb6060 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -382,7 +382,8 @@ async def test_hmip_heating_group_heat_with_radiator(hass, default_mock_hap_fact entity_name = "Vorzimmer" device_model = None mock_hap = await default_mock_hap_factory.async_get_mock_hap( - test_devices=["Heizkörperthermostat2"], test_groups=[entity_name], + test_devices=["Heizkörperthermostat2"], + test_groups=[entity_name], ) ha_state, hmip_device = get_and_check_entity_basics( hass, mock_hap, entity_id, entity_name, device_model diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 5b201da8aa8..37b293a3452 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -125,7 +125,9 @@ async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry with patch( "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state", side_effect=Exception, - ), patch("homematicip.aio.connection.AsyncConnection.init",): + ), patch( + "homematicip.aio.connection.AsyncConnection.init", + ): assert await async_setup_component(hass, HMIPC_DOMAIN, {}) assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id] diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 45853687f13..2946c0b383c 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -330,7 +330,8 @@ async def test_x_forwarded_proto_with_multiple_headers(aiohttp_client, caplog): @pytest.mark.parametrize( - "x_forwarded_proto", ["", ",", "https, , https", "https, https, "], + "x_forwarded_proto", + ["", ",", "https, , https", "https, https, "], ) async def test_x_forwarded_proto_empty_element( x_forwarded_proto, aiohttp_client, caplog @@ -342,7 +343,8 @@ async def test_x_forwarded_proto_empty_element( mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( - "/", headers={X_FORWARDED_FOR: "1.1.1.1", X_FORWARDED_PROTO: x_forwarded_proto}, + "/", + headers={X_FORWARDED_FOR: "1.1.1.1", X_FORWARDED_PROTO: x_forwarded_proto}, ) assert resp.status == 400 diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 42a6f525319..21bb489ffe7 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -115,7 +115,8 @@ async def test_manual_flow_works(hass, aioclient_mock): ) with patch( - "aiohue.Bridge", return_value=bridge, + "aiohue.Bridge", + return_value=bridge, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "2.2.2.2"} @@ -148,7 +149,8 @@ async def test_manual_flow_bridge_exist(hass, aioclient_mock): ).add_to_hass(hass) with patch( - "homeassistant.components.hue.config_flow.discover_nupnp", return_value=[], + "homeassistant.components.hue.config_flow.discover_nupnp", + return_value=[], ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": "user"} @@ -162,7 +164,8 @@ async def test_manual_flow_bridge_exist(hass, aioclient_mock): ) with patch( - "aiohue.Bridge", return_value=bridge, + "aiohue.Bridge", + return_value=bridge, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "2.2.2.2"} @@ -293,7 +296,9 @@ async def test_flow_link_timeout(hass): async def test_flow_link_unknown_error(hass): """Test if a unknown error happened during the linking processes.""" - mock_bridge = get_mock_bridge(mock_create_user=AsyncMock(side_effect=OSError),) + mock_bridge = get_mock_bridge( + mock_create_user=AsyncMock(side_effect=OSError), + ) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", return_value=[mock_bridge], @@ -481,7 +486,9 @@ async def test_bridge_ssdp_already_configured(hass): async def test_import_with_no_config(hass): """Test importing a host without an existing config file.""" result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "import"}, data={"host": "0.0.0.0"}, + const.DOMAIN, + context={"source": "import"}, + data={"host": "0.0.0.0"}, ) assert result["type"] == "form" @@ -496,12 +503,16 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): all existing entries that either have same IP or same bridge_id. """ orig_entry = MockConfigEntry( - domain="hue", data={"host": "0.0.0.0", "username": "aaaa"}, unique_id="id-1234", + domain="hue", + data={"host": "0.0.0.0", "username": "aaaa"}, + unique_id="id-1234", ) orig_entry.add_to_hass(hass) MockConfigEntry( - domain="hue", data={"host": "1.2.3.4", "username": "bbbb"}, unique_id="id-5678", + domain="hue", + data={"host": "1.2.3.4", "username": "bbbb"}, + unique_id="id-5678", ).add_to_hass(hass) assert len(hass.config_entries.async_entries("hue")) == 2 @@ -511,7 +522,8 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): ) with patch( - "aiohue.Bridge", return_value=bridge, + "aiohue.Bridge", + return_value=bridge, ): result = await hass.config_entries.flow.async_init( "hue", data={"host": "2.2.2.2"}, context={"source": "import"} @@ -614,7 +626,9 @@ async def test_ssdp_discovery_update_configuration(hass): async def test_options_flow(hass): """Test options config flow.""" entry = MockConfigEntry( - domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"}, + domain="hue", + unique_id="aabbccddeeff", + data={"host": "0.0.0.0"}, ) entry.add_to_hass(hass) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index b1dc024998e..1d6db3498f1 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -196,7 +196,9 @@ async def test_fixing_unique_id_other_ignored(hass, mock_bridge_setup): source=config_entries.SOURCE_IGNORE, ).add_to_hass(hass) entry = MockConfigEntry( - domain=hue.DOMAIN, data={"host": "0.0.0.0"}, unique_id="invalid-id", + domain=hue.DOMAIN, + data={"host": "0.0.0.0"}, + unique_id="invalid-id", ) entry.add_to_hass(hass) assert await async_setup_component(hass, hue.DOMAIN, {}) is True @@ -208,11 +210,15 @@ async def test_fixing_unique_id_other_ignored(hass, mock_bridge_setup): async def test_fixing_unique_id_other_correct(hass, mock_bridge_setup): """Test we remove config entry if another one has correct ID.""" correct_entry = MockConfigEntry( - domain=hue.DOMAIN, data={"host": "0.0.0.0"}, unique_id="mock-id", + domain=hue.DOMAIN, + data={"host": "0.0.0.0"}, + unique_id="mock-id", ) correct_entry.add_to_hass(hass) entry = MockConfigEntry( - domain=hue.DOMAIN, data={"host": "0.0.0.0"}, unique_id="invalid-id", + domain=hue.DOMAIN, + data={"host": "0.0.0.0"}, + unique_id="invalid-id", ) entry.add_to_hass(hass) assert await async_setup_component(hass, hue.DOMAIN, {}) is True diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 383d0445a7d..37fb150ddf7 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -43,7 +43,8 @@ async def test_user_form(hass): return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.2.3.4"}, + result["flow_id"], + {"host": "1.2.3.4"}, ) assert result2["type"] == "create_entry" @@ -62,7 +63,8 @@ async def test_user_form(hass): assert result3["errors"] == {} result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], {"host": "1.2.3.4"}, + result3["flow_id"], + {"host": "1.2.3.4"}, ) assert result4["type"] == "abort" @@ -175,7 +177,8 @@ async def test_form_cannot_connect(hass): return_value=mock_powerview_userdata, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.2.3.4"}, + result["flow_id"], + {"host": "1.2.3.4"}, ) assert result2["type"] == "form" @@ -194,7 +197,8 @@ async def test_form_no_data(hass): return_value=mock_powerview_userdata, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.2.3.4"}, + result["flow_id"], + {"host": "1.2.3.4"}, ) assert result2["type"] == "form" @@ -213,7 +217,8 @@ async def test_form_unknown_exception(hass): return_value=mock_powerview_userdata, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.2.3.4"}, + result["flow_id"], + {"host": "1.2.3.4"}, ) assert result2["type"] == "form" diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 3f9098abfc8..1e37ff2e021 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -33,11 +33,13 @@ async def test_user_flow(hass): "homeassistant.components.hvv_departures.hub.GTI.init", return_value=FIXTURE_INIT, ), patch("pygti.gti.GTI.checkName", return_value=FIXTURE_CHECK_NAME,), patch( - "pygti.gti.GTI.stationInformation", return_value=FIXTURE_STATION_INFORMATION, + "pygti.gti.GTI.stationInformation", + return_value=FIXTURE_STATION_INFORMATION, ), patch( "homeassistant.components.hvv_departures.async_setup", return_value=True ), patch( - "homeassistant.components.hvv_departures.async_setup_entry", return_value=True, + "homeassistant.components.hvv_departures.async_setup_entry", + return_value=True, ): # step: user @@ -56,14 +58,16 @@ async def test_user_flow(hass): # step: station result_station = await hass.config_entries.flow.async_configure( - result_user["flow_id"], {CONF_STATION: "Wartenau"}, + result_user["flow_id"], + {CONF_STATION: "Wartenau"}, ) assert result_station["step_id"] == "station_select" # step: station_select result_station_select = await hass.config_entries.flow.async_configure( - result_user["flow_id"], {CONF_STATION: "Wartenau"}, + result_user["flow_id"], + {CONF_STATION: "Wartenau"}, ) assert result_station_select["type"] == "create_entry" @@ -92,11 +96,13 @@ async def test_user_flow_no_results(hass): "homeassistant.components.hvv_departures.hub.GTI.init", return_value=FIXTURE_INIT, ), patch( - "pygti.gti.GTI.checkName", return_value={"returnCode": "OK", "results": []}, + "pygti.gti.GTI.checkName", + return_value={"returnCode": "OK", "results": []}, ), patch( "homeassistant.components.hvv_departures.async_setup", return_value=True ), patch( - "homeassistant.components.hvv_departures.async_setup_entry", return_value=True, + "homeassistant.components.hvv_departures.async_setup_entry", + return_value=True, ): # step: user @@ -115,7 +121,8 @@ async def test_user_flow_no_results(hass): # step: station result_station = await hass.config_entries.flow.async_configure( - result_user["flow_id"], {CONF_STATION: "non_existing_station"}, + result_user["flow_id"], + {CONF_STATION: "non_existing_station"}, ) assert result_station["step_id"] == "station" @@ -176,9 +183,11 @@ async def test_user_flow_station(hass): """Test that config flow handles empty data on step station.""" with patch( - "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True, + "homeassistant.components.hvv_departures.hub.GTI.init", + return_value=True, ), patch( - "pygti.gti.GTI.checkName", return_value={"returnCode": "OK", "results": []}, + "pygti.gti.GTI.checkName", + return_value={"returnCode": "OK", "results": []}, ): # step: user @@ -197,7 +206,8 @@ async def test_user_flow_station(hass): # step: station result_station = await hass.config_entries.flow.async_configure( - result_user["flow_id"], None, + result_user["flow_id"], + None, ) assert result_station["type"] == "form" assert result_station["step_id"] == "station" @@ -207,9 +217,11 @@ async def test_user_flow_station_select(hass): """Test that config flow handles empty data on step station_select.""" with patch( - "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True, + "homeassistant.components.hvv_departures.hub.GTI.init", + return_value=True, ), patch( - "pygti.gti.GTI.checkName", return_value=FIXTURE_CHECK_NAME, + "pygti.gti.GTI.checkName", + return_value=FIXTURE_CHECK_NAME, ): result_user = await hass.config_entries.flow.async_init( DOMAIN, @@ -222,12 +234,14 @@ async def test_user_flow_station_select(hass): ) result_station = await hass.config_entries.flow.async_configure( - result_user["flow_id"], {CONF_STATION: "Wartenau"}, + result_user["flow_id"], + {CONF_STATION: "Wartenau"}, ) # step: station_select result_station_select = await hass.config_entries.flow.async_configure( - result_station["flow_id"], None, + result_station["flow_id"], + None, ) assert result_station_select["type"] == "form" @@ -251,9 +265,11 @@ async def test_options_flow(hass): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.hvv_departures.hub.GTI.init", return_value=True, + "homeassistant.components.hvv_departures.hub.GTI.init", + return_value=True, ), patch( - "pygti.gti.GTI.departureList", return_value=FIXTURE_DEPARTURE_LIST, + "pygti.gti.GTI.departureList", + return_value=FIXTURE_DEPARTURE_LIST, ): assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -332,7 +348,8 @@ async def test_options_flow_cannot_connect(hass): config_entry.add_to_hass(hass) with patch( - "pygti.gti.GTI.departureList", side_effect=CannotConnect(), + "pygti.gti.GTI.departureList", + side_effect=CannotConnect(), ): assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 72edcd7160a..c006a262e85 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -69,7 +69,8 @@ class TestImageProcessing: self.hass.stop() @patch( - "homeassistant.components.demo.camera.Path.read_bytes", return_value=b"Test", + "homeassistant.components.demo.camera.Path.read_bytes", + return_value=b"Test", ) def test_get_image_from_camera(self, mock_camera_read): """Grab an image from camera entity.""" diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 7e2a9f5d9c3..973b21a8c89 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -41,7 +41,8 @@ def mock_batch_timeout(hass, monkeypatch): """Mock the event bus listener and the batch timeout for tests.""" hass.bus.listen = MagicMock() monkeypatch.setattr( - f"{INFLUX_PATH}.InfluxThread.batch_timeout", Mock(return_value=0), + f"{INFLUX_PATH}.InfluxThread.batch_timeout", + Mock(return_value=0), ) @@ -868,7 +869,11 @@ async def test_event_listener_default_measurement( handler_method = await _setup(hass, mock_client, config, get_write_api) state = MagicMock( - state=1, domain="fake", entity_id="fake.ok", object_id="ok", attributes={}, + state=1, + domain="fake", + entity_id="fake.ok", + object_id="ok", + attributes={}, ) event = MagicMock(data={"new_state": state}, time_fired=12345) body = [ diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 150e378e383..633f9e891f8 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -356,7 +356,12 @@ async def test_state_matches_first_query_result_for_multiple_return( @pytest.mark.parametrize( "mock_client, config_ext, queries, set_query_mock", [ - (DEFAULT_API_VERSION, BASE_V1_CONFIG, BASE_V1_QUERY, _set_query_mock_v1,), + ( + 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"], @@ -577,7 +582,12 @@ async def test_connection_error_at_startup( indirect=["mock_client"], ) async def test_data_repository_not_found( - hass, caplog, mock_client, config_ext, queries, set_query_mock, + hass, + caplog, + mock_client, + config_ext, + queries, + set_query_mock, ): """Test sensor is not setup when bucket not available.""" set_query_mock(mock_client) diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 9b063d038eb..9252f5edad3 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -130,7 +130,8 @@ async def _init_form(hass, modem_type): assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {MODEM_TYPE: modem_type}, + result["flow_id"], + {MODEM_TYPE: modem_type}, ) return result2 @@ -140,7 +141,8 @@ async def _device_form(hass, flow_id, connection, user_input): with patch(PATCH_CONNECTION, new=connection,), patch( PATCH_ASYNC_SETUP, return_value=True ) as mock_setup, patch( - PATCH_ASYNC_SETUP_ENTRY, return_value=True, + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure(flow_id, user_input) return result, mock_setup, mock_setup_entry @@ -397,7 +399,8 @@ async def _options_init_form(hass, entry_id, step): assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( - result["flow_id"], {step: True}, + result["flow_id"], + {step: True}, ) return result2 @@ -673,7 +676,8 @@ async def test_options_dup_selection(hass: HomeAssistantType): assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( - result["flow_id"], {STEP_ADD_OVERRIDE: True, STEP_ADD_X10: True}, + result["flow_id"], + {STEP_ADD_OVERRIDE: True, STEP_ADD_X10: True}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "select_single"} diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index b3b8968f451..b45c4e89529 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -79,7 +79,9 @@ async def test_flow_entry_created_from_user_input(): with patch( "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" ) as config_form, patch.object( - flow.hass.config_entries, "async_entries", return_value=[], + flow.hass.config_entries, + "async_entries", + return_value=[], ) as config_entries: result = await flow.async_step_user(user_input=test_data) diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 7133bf3cde7..19ee2f73231 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -24,7 +24,8 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" @@ -39,7 +40,9 @@ async def test_show_zeroconf_form( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["step_id"] == "zeroconf_confirm" @@ -55,7 +58,9 @@ async def test_connection_error( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, ) assert result["step_id"] == "user" @@ -71,7 +76,9 @@ async def test_zeroconf_connection_error( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -101,7 +108,9 @@ async def test_user_connection_upgrade_required( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, ) assert result["step_id"] == "user" @@ -117,7 +126,9 @@ async def test_zeroconf_connection_upgrade_required( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -132,7 +143,9 @@ async def test_user_parse_error( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_ABORT @@ -147,7 +160,9 @@ async def test_zeroconf_parse_error( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -162,7 +177,9 @@ async def test_user_ipp_error( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_ABORT @@ -177,7 +194,9 @@ async def test_zeroconf_ipp_error( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -192,7 +211,9 @@ async def test_user_ipp_version_error( user_input = {**MOCK_USER_INPUT} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_ABORT @@ -207,7 +228,9 @@ async def test_zeroconf_ipp_version_error( discovery_info = {**MOCK_ZEROCONF_IPP_SERVICE_INFO} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -222,7 +245,9 @@ async def test_user_device_exists_abort( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, + context={"source": SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_ABORT @@ -237,7 +262,9 @@ async def test_zeroconf_device_exists_abort( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -258,7 +285,9 @@ async def test_zeroconf_with_uuid_device_exists_abort( }, } result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -276,7 +305,9 @@ async def test_zeroconf_empty_unique_id( "properties": {**MOCK_ZEROCONF_IPP_SERVICE_INFO["properties"], "UUID": ""}, } result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_FORM @@ -290,7 +321,9 @@ async def test_zeroconf_no_unique_id( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_FORM @@ -303,7 +336,8 @@ async def test_full_user_flow_implementation( mock_connection(aioclient_mock) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" @@ -336,7 +370,9 @@ async def test_full_zeroconf_flow_implementation( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["step_id"] == "zeroconf_confirm" @@ -370,7 +406,9 @@ async def test_full_zeroconf_tls_flow_implementation( discovery_info = MOCK_ZEROCONF_IPPS_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=discovery_info, ) assert result["step_id"] == "zeroconf_confirm" diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 204075ecb8c..4ffd6e4f25d 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -72,7 +72,11 @@ async def test_import(hass): async def test_integration_already_configured(hass): """Test integration is already configured.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, options={},) + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, context={"source": "user"} diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 984bbbf7c75..fd0de854056 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -39,7 +39,10 @@ async def test_setup_with_config(hass, legacy_patchable_time): async def test_successful_config_entry(hass, legacy_patchable_time): """Test that Islamic Prayer Times is configured successfully.""" - entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) + entry = MockConfigEntry( + domain=islamic_prayer_times.DOMAIN, + data={}, + ) entry.add_to_hass(hass) with patch( @@ -58,7 +61,10 @@ async def test_successful_config_entry(hass, legacy_patchable_time): async def test_setup_failed(hass, legacy_patchable_time): """Test Islamic Prayer Times failed due to an error.""" - entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) + entry = MockConfigEntry( + domain=islamic_prayer_times.DOMAIN, + data={}, + ) entry.add_to_hass(hass) # test request error raising ConfigEntryNotReady @@ -73,7 +79,10 @@ async def test_setup_failed(hass, legacy_patchable_time): async def test_unload_entry(hass, legacy_patchable_time): """Test removing Islamic Prayer Times.""" - entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={},) + entry = MockConfigEntry( + domain=islamic_prayer_times.DOMAIN, + data={}, + ) entry.add_to_hass(hass) with patch( diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 55311494d30..cbbc7427f9f 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -83,13 +83,15 @@ async def test_form(hass: HomeAssistantType): ) as mock_connection_class, patch( PATCH_ASYNC_SETUP, return_value=True ) as mock_setup, patch( - PATCH_ASYNC_SETUP_ENTRY, return_value=True, + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, ) as mock_setup_entry: isy_conn = mock_connection_class.return_value isy_conn.get_config.return_value = None mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_USER_INPUT, + result["flow_id"], + MOCK_USER_INPUT, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" @@ -126,10 +128,12 @@ async def test_form_invalid_auth(hass: HomeAssistantType): DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch(PATCH_CONFIGURATION), patch( - PATCH_CONNECTION, side_effect=ValueError("PyISY could not connect to the ISY."), + PATCH_CONNECTION, + side_effect=ValueError("PyISY could not connect to the ISY."), ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_USER_INPUT, + result["flow_id"], + MOCK_USER_INPUT, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -142,10 +146,12 @@ async def test_form_cannot_connect(hass: HomeAssistantType): DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch(PATCH_CONFIGURATION), patch( - PATCH_CONNECTION, side_effect=CannotConnect, + PATCH_CONNECTION, + side_effect=CannotConnect, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_USER_INPUT, + result["flow_id"], + MOCK_USER_INPUT, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -169,7 +175,8 @@ async def test_form_existing_config_entry(hass: HomeAssistantType): isy_conn.get_config.return_value = None mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_USER_INPUT, + result["flow_id"], + MOCK_USER_INPUT, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -179,13 +186,16 @@ async def test_import_flow_some_fields(hass: HomeAssistantType) -> None: with patch(PATCH_CONFIGURATION) as mock_config_class, patch( PATCH_CONNECTION ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( - PATCH_ASYNC_SETUP_ENTRY, return_value=True, + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, ): isy_conn = mock_connection_class.return_value isy_conn.get_config.return_value = None mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_BASIC_CONFIG, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=MOCK_IMPORT_BASIC_CONFIG, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -200,13 +210,16 @@ async def test_import_flow_with_https(hass: HomeAssistantType) -> None: with patch(PATCH_CONFIGURATION) as mock_config_class, patch( PATCH_CONNECTION ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( - PATCH_ASYNC_SETUP_ENTRY, return_value=True, + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, ): isy_conn = mock_connection_class.return_value isy_conn.get_config.return_value = None mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_WITH_SSL, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=MOCK_IMPORT_WITH_SSL, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -220,13 +233,16 @@ async def test_import_flow_all_fields(hass: HomeAssistantType) -> None: with patch(PATCH_CONFIGURATION) as mock_config_class, patch( PATCH_CONNECTION ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( - PATCH_ASYNC_SETUP_ENTRY, return_value=True, + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, ): isy_conn = mock_connection_class.return_value isy_conn.get_config.return_value = None mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_FULL_CONFIG, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=MOCK_IMPORT_FULL_CONFIG, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -284,13 +300,15 @@ async def test_form_ssdp(hass: HomeAssistantType): ) as mock_connection_class, patch( PATCH_ASYNC_SETUP, return_value=True ) as mock_setup, patch( - PATCH_ASYNC_SETUP_ENTRY, return_value=True, + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, ) as mock_setup_entry: isy_conn = mock_connection_class.return_value isy_conn.get_config.return_value = None mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_USER_INPUT, + result["flow_id"], + MOCK_USER_INPUT, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index 72b80e75bcf..df5ec18db8a 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -57,7 +57,8 @@ async def test_found(hass, mock_disco): mock_disco.pi_disco.controllers["blah"] = object() with patch( - "homeassistant.components.izone.climate.async_setup_entry", return_value=True, + "homeassistant.components.izone.climate.async_setup_entry", + return_value=True, ) as mock_setup, patch( "homeassistant.components.izone.config_flow.async_start_discovery_service" ) as start_disco, patch( diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index 6ea2ba9d283..4fd61ede8ba 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -39,14 +39,16 @@ async def user_flow(hass): async def test_user_flow(hass, user_flow): """Test a successful user initiated flow.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ), patch( "homeassistant.components.kodi.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.kodi.async_setup_entry", return_value=True, + "homeassistant.components.kodi.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure(user_flow, TEST_HOST) @@ -82,14 +84,16 @@ async def test_form_valid_auth(hass, user_flow): assert result["errors"] == {} with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ), patch( "homeassistant.components.kodi.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.kodi.async_setup_entry", return_value=True, + "homeassistant.components.kodi.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CREDENTIALS @@ -113,9 +117,12 @@ async def test_form_valid_auth(hass, user_flow): async def test_form_valid_ws_port(hass, user_flow): """Test we handle valid websocket port.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch.object( - MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", new=get_kodi_connection, @@ -127,14 +134,16 @@ async def test_form_valid_ws_port(hass, user_flow): assert result["errors"] == {} with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ), patch( "homeassistant.components.kodi.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.kodi.async_setup_entry", return_value=True, + "homeassistant.components.kodi.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_WS_PORT @@ -202,7 +211,8 @@ async def test_form_invalid_auth(hass, user_flow): assert result["errors"] == {"base": "cannot_connect"} with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=Exception, + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=Exception, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), @@ -216,9 +226,12 @@ async def test_form_invalid_auth(hass, user_flow): assert result["errors"] == {"base": "unknown"} with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch.object( - MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", new=get_kodi_connection, @@ -251,7 +264,8 @@ async def test_form_cannot_connect_http(hass, user_flow): async def test_form_exception_http(hass, user_flow): """Test we handle generic exception over HTTP.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=Exception, + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=Exception, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), @@ -266,9 +280,12 @@ async def test_form_exception_http(hass, user_flow): async def test_form_cannot_connect_ws(hass, user_flow): """Test we handle cannot connect over WebSocket error.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch.object( - MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", new=get_kodi_connection, @@ -280,7 +297,8 @@ async def test_form_cannot_connect_ws(hass, user_flow): assert result["errors"] == {} with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch.object( MockWSConnection, "connected", new_callable=PropertyMock(return_value=False) ), patch( @@ -314,9 +332,12 @@ async def test_form_cannot_connect_ws(hass, user_flow): async def test_form_exception_ws(hass, user_flow): """Test we handle generic exception over WebSocket.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch.object( - MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", new=get_kodi_connection, @@ -328,7 +349,8 @@ async def test_form_exception_ws(hass, user_flow): assert result["errors"] == {} with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch.object( MockWSConnection, "connect", AsyncMock(side_effect=Exception) ), patch( @@ -347,7 +369,8 @@ async def test_form_exception_ws(hass, user_flow): async def test_discovery(hass): """Test discovery flow works.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), @@ -362,7 +385,8 @@ async def test_discovery(hass): with patch( "homeassistant.components.kodi.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.kodi.async_setup_entry", return_value=True, + "homeassistant.components.kodi.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={} @@ -404,9 +428,12 @@ async def test_discovery_cannot_connect_http(hass): async def test_discovery_cannot_connect_ws(hass): """Test discovery aborts if cannot connect to websocket.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch.object( - MockWSConnection, "connect", AsyncMock(side_effect=CannotConnectError), + MockWSConnection, + "connect", + AsyncMock(side_effect=CannotConnectError), ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", new=get_kodi_connection, @@ -423,7 +450,8 @@ async def test_discovery_cannot_connect_ws(hass): async def test_discovery_exception_http(hass, user_flow): """Test we handle generic exception during discovery validation.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=Exception, + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=Exception, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), @@ -457,7 +485,8 @@ async def test_discovery_invalid_auth(hass): async def test_discovery_duplicate_data(hass): """Test discovery aborts if same mDNS packet arrives.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), @@ -502,17 +531,21 @@ async def test_discovery_updates_unique_id(hass): async def test_form_import(hass): """Test we get the form with import source.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", return_value=True, + "homeassistant.components.kodi.config_flow.Kodi.ping", + return_value=True, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ), patch( "homeassistant.components.kodi.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.kodi.async_setup_entry", return_value=True, + "homeassistant.components.kodi.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=TEST_IMPORT, + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TEST_IMPORT, ) assert result["type"] == "create_entry" @@ -534,7 +567,9 @@ async def test_form_import_invalid_auth(hass): return_value=MockConnection(), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_IMPORT, + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TEST_IMPORT, ) assert result["type"] == "abort" @@ -551,7 +586,9 @@ async def test_form_import_cannot_connect(hass): return_value=MockConnection(), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_IMPORT, + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TEST_IMPORT, ) assert result["type"] == "abort" @@ -561,13 +598,16 @@ async def test_form_import_cannot_connect(hass): async def test_form_import_exception(hass): """Test we handle unknown exception on import.""" with patch( - "homeassistant.components.kodi.config_flow.Kodi.ping", side_effect=Exception, + "homeassistant.components.kodi.config_flow.Kodi.ping", + side_effect=Exception, ), patch( "homeassistant.components.kodi.config_flow.get_kodi_connection", return_value=MockConnection(), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TEST_IMPORT, + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=TEST_IMPORT, ) assert result["type"] == "abort" diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py index f091343d8a3..5819f2128c8 100644 --- a/tests/components/kodi/test_device_trigger.py +++ b/tests/components/kodi/test_device_trigger.py @@ -48,7 +48,8 @@ async def test_get_triggers(hass, device_reg, entity_reg): config_entry = MockConfigEntry(domain=DOMAIN, data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "host", 1234)}, + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "host", 1234)}, ) entity_reg.async_get_or_create(MP_DOMAIN, DOMAIN, "5678", device_id=device_entry.id) expected_triggers = [ @@ -113,7 +114,10 @@ async def test_if_fires_on_state_change(hass, calls, kodi_media_player): ) await hass.services.async_call( - MP_DOMAIN, "turn_on", {"entity_id": kodi_media_player}, blocking=True, + MP_DOMAIN, + "turn_on", + {"entity_id": kodi_media_player}, + blocking=True, ) await hass.async_block_till_done() @@ -121,7 +125,10 @@ async def test_if_fires_on_state_change(hass, calls, kodi_media_player): assert calls[0].data["some"] == f"turn_on - {kodi_media_player}" await hass.services.async_call( - MP_DOMAIN, "turn_off", {"entity_id": kodi_media_player}, blocking=True, + MP_DOMAIN, + "turn_off", + {"entity_id": kodi_media_player}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 62a159538a0..dfd4600f1fb 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -1108,7 +1108,8 @@ async def test_option_flow_import(hass, mock_panel): assert schema["8"] == "Disabled" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={}, + result["flow_id"], + user_input={}, ) assert result["type"] == "form" assert result["step_id"] == "options_binary" @@ -1129,7 +1130,8 @@ async def test_option_flow_import(hass, mock_panel): assert schema["type"] == "ds18b20" assert schema["name"] == "temper" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"type": "dht"}, + result["flow_id"], + user_input={"type": "dht"}, ) assert result["type"] == "form" assert result["step_id"] == "options_switch" diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index d36b937d3c3..c198812a82b 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -387,7 +387,8 @@ async def test_config_passed_to_config_entry(hass): async def test_unload_entry(hass, mock_panel): """Test being able to unload an entry.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) entry = MockConfigEntry( domain=konnected.DOMAIN, data={konnected.CONF_ID: "aabbccddeeff"} @@ -570,7 +571,8 @@ async def test_api(hass, aiohttp_client, mock_panel): async def test_state_updates_zone(hass, aiohttp_client, mock_panel): """Test callback view.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) device_config = config_flow.CONFIG_ENTRY_SCHEMA( @@ -720,7 +722,8 @@ async def test_state_updates_zone(hass, aiohttp_client, mock_panel): async def test_state_updates_pin(hass, aiohttp_client, mock_panel): """Test callback view.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) device_config = config_flow.CONFIG_ENTRY_SCHEMA( @@ -777,7 +780,11 @@ async def test_state_updates_pin(hass, aiohttp_client, mock_panel): entry.add_to_hass(hass) # Add empty data field to ensure we process it correctly (possible if entry is ignored) - entry = MockConfigEntry(domain="konnected", title="Konnected Alarm Panel", data={},) + entry = MockConfigEntry( + domain="konnected", + title="Konnected Alarm Panel", + data={}, + ) entry.add_to_hass(hass) assert ( diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 1f3f0c22bd0..aeedde82af5 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -107,7 +107,10 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create( - DOMAIN, "test", "5678", device_id=device_entry.id, + DOMAIN, + "test", + "5678", + device_id=device_entry.id, ) actions = await async_get_device_automations(hass, "action", device_entry.id) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 1660ec422f3..a3f24bef3c9 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -589,28 +589,40 @@ async def test_light_brightness_pct_conversion(hass): assert state.attributes["brightness"] == 100 await hass.services.async_call( - "light", "turn_on", {"entity_id": entity.entity_id, "brightness_pct": 1}, True, + "light", + "turn_on", + {"entity_id": entity.entity_id, "brightness_pct": 1}, + True, ) _, data = entity.last_call("turn_on") assert data["brightness"] == 3, data await hass.services.async_call( - "light", "turn_on", {"entity_id": entity.entity_id, "brightness_pct": 2}, True, + "light", + "turn_on", + {"entity_id": entity.entity_id, "brightness_pct": 2}, + True, ) _, data = entity.last_call("turn_on") assert data["brightness"] == 5, data await hass.services.async_call( - "light", "turn_on", {"entity_id": entity.entity_id, "brightness_pct": 50}, True, + "light", + "turn_on", + {"entity_id": entity.entity_id, "brightness_pct": 50}, + True, ) _, data = entity.last_call("turn_on") assert data["brightness"] == 128, data await hass.services.async_call( - "light", "turn_on", {"entity_id": entity.entity_id, "brightness_pct": 99}, True, + "light", + "turn_on", + {"entity_id": entity.entity_id, "brightness_pct": 99}, + True, ) _, data = entity.last_call("turn_on") diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index f6b3ebf6c8e..e3a9ecd9ef8 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -23,7 +23,10 @@ async def test_config_flow(hass): async def test_already_setup(hass): """Test we abort if already setup.""" - MockConfigEntry(domain=DOMAIN, data={},).add_to_hass(hass) + MockConfigEntry( + domain=DOMAIN, + data={}, + ).add_to_hass(hass) # Should fail, same NAME result = await hass.config_entries.flow.async_init( diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 0f3d17c9b59..2fa68778f7e 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -35,7 +35,8 @@ async def locative_client(loop, hass, hass_client): async def webhook_id(hass, locative_client): """Initialize the Geofency component and get the webhook_id.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( "locative", context={"source": "user"} diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index fc9c5fe279d..4ec5f14237c 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -50,7 +50,8 @@ async def test_bridge_import_flow(hass): } with patch( - "homeassistant.components.lutron_caseta.async_setup_entry", return_value=True, + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, ) as mock_setup_entry, patch( "homeassistant.components.lutron_caseta.async_setup", return_value=True ), patch.object( @@ -143,7 +144,8 @@ async def test_duplicate_bridge_import(hass): mock_entry.add_to_hass(hass) with patch( - "homeassistant.components.lutron_caseta.async_setup_entry", return_value=True, + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, ) as mock_setup_entry: # Mock entry added, try initializing flow with duplicate host result = await hass.config_entries.flow.async_init( diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index 5d6cec844f2..fd244d87a8f 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -31,7 +31,8 @@ async def webhook_id_with_api_key(hass): ) await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( "mailgun", context={"source": "user"} @@ -50,7 +51,8 @@ async def webhook_id_without_api_key(hass): await async_setup_component(hass, mailgun.DOMAIN, {}) await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( "mailgun", context={"source": "user"} diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index 7e174a4f8a0..b9e09a5d769 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -18,5 +18,6 @@ async def test_setup(hass): assert melissa.DATA_MELISSA in hass.data assert isinstance( - hass.data[melissa.DATA_MELISSA], type(mocked_melissa.return_value), + hass.data[melissa.DATA_MELISSA], + type(mocked_melissa.return_value), ) diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 650a88df84e..3aa70aa48b3 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -84,9 +84,11 @@ def mock_controller_client_single(): def mock_setup(): """Prevent setup.""" with patch( - "homeassistant.components.meteo_france.async_setup", return_value=True, + "homeassistant.components.meteo_france.async_setup", + return_value=True, ), patch( - "homeassistant.components.meteo_france.async_setup_entry", return_value=True, + "homeassistant.components.meteo_france.async_setup_entry", + return_value=True, ): yield @@ -123,7 +125,9 @@ async def test_user(hass, client_single): # test with all provided with search returning only 1 place result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_CITY: CITY_1_POSTAL}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["result"].unique_id == f"{CITY_1_LAT}, {CITY_1_LON}" @@ -137,7 +141,9 @@ async def test_user_list(hass, client_multiple): # test with all provided with search returning more than 1 place result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_2_NAME}, + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_CITY: CITY_2_NAME}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "cities" @@ -157,7 +163,9 @@ async def test_import(hass, client_multiple): """Test import step.""" # import with all result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_2_NAME}, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_CITY: CITY_2_NAME}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["result"].unique_id == f"{CITY_2_LAT}, {CITY_2_LON}" @@ -169,7 +177,9 @@ async def test_import(hass, client_multiple): async def test_search_failed(hass, client_empty): """Test error displayed if no result in search.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_CITY: CITY_1_POSTAL}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -186,14 +196,18 @@ async def test_abort_if_already_setup(hass, client_single): # Should fail, same CITY same postal code (import) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL}, + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_CITY: CITY_1_POSTAL}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" # Should fail, same CITY same postal code (flow) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_CITY: CITY_1_POSTAL}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -216,7 +230,8 @@ async def test_options_flow(hass: HomeAssistantType): # Default result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={}, + result["flow_id"], + user_input={}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_MODE] == FORECAST_MODE_DAILY @@ -224,7 +239,8 @@ async def test_options_flow(hass: HomeAssistantType): # Manual result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_MODE: FORECAST_MODE_HOURLY}, + result["flow_id"], + user_input={CONF_MODE: FORECAST_MODE_HOURLY}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_MODE] == FORECAST_MODE_HOURLY diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index 6916e949b1c..5987d44ac27 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -36,7 +36,8 @@ async def test_form(hass, requests_mock): with patch( "homeassistant.components.metoffice.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.metoffice.async_setup_entry", return_value=True, + "homeassistant.components.metoffice.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_key": TEST_API_KEY} @@ -67,7 +68,8 @@ async def test_form_already_configured(hass, requests_mock): requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=3hourly", text="", + "/public/data/val/wxfcs/all/json/354107?res=3hourly", + text="", ) MockConfigEntry( @@ -98,7 +100,8 @@ async def test_form_cannot_connect(hass, requests_mock): ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"api_key": TEST_API_KEY}, + result["flow_id"], + {"api_key": TEST_API_KEY}, ) assert result2["type"] == "form" @@ -115,7 +118,8 @@ async def test_form_unknown_error(hass, mock_simple_manager_fail): ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"api_key": TEST_API_KEY}, + result["flow_id"], + {"api_key": TEST_API_KEY}, ) assert result2["type"] == "form" diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index b04dc6b2422..43dbf3f75e0 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -20,7 +20,8 @@ from tests.common import MockConfigEntry, load_fixture @patch( - "datapoint.Forecast.datetime.datetime", NewDateTime, + "datapoint.Forecast.datetime.datetime", + NewDateTime, ) async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_time): """Test the Met Office sensor platform.""" @@ -31,10 +32,14 @@ async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_tim requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly, + "/public/data/val/wxfcs/all/json/354107?res=3hourly", + text=wavertree_hourly, ) - entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,) + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -57,7 +62,8 @@ async def test_one_sensor_site_running(hass, requests_mock, legacy_patchable_tim @patch( - "datapoint.Forecast.datetime.datetime", NewDateTime, + "datapoint.Forecast.datetime.datetime", + NewDateTime, ) 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.""" @@ -76,10 +82,16 @@ async def test_two_sensor_sites_running(hass, requests_mock, legacy_patchable_ti "/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly ) - entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,) + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - entry2 = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_KINGSLYNN,) + entry2 = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_KINGSLYNN, + ) entry2.add_to_hass(hass) await hass.config_entries.async_setup(entry2.entry_id) await hass.async_block_till_done() diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 673dec7d5a6..f1530021fcf 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -18,7 +18,8 @@ from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture @patch( - "datapoint.Forecast.datetime.datetime", NewDateTime, + "datapoint.Forecast.datetime.datetime", + NewDateTime, ) async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time): """Test we handle cannot connect error.""" @@ -26,7 +27,10 @@ async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time): requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text="") requests_mock.get("/public/data/val/wxfcs/all/json/354107?res=3hourly", text="") - entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,) + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -39,7 +43,8 @@ async def test_site_cannot_connect(hass, requests_mock, legacy_patchable_time): @patch( - "datapoint.Forecast.datetime.datetime", NewDateTime, + "datapoint.Forecast.datetime.datetime", + NewDateTime, ) async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): """Test we handle cannot connect error.""" @@ -54,7 +59,10 @@ async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): "/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly ) - entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,) + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -73,7 +81,8 @@ async def test_site_cannot_update(hass, requests_mock, legacy_patchable_time): @patch( - "datapoint.Forecast.datetime.datetime", NewDateTime, + "datapoint.Forecast.datetime.datetime", + NewDateTime, ) async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_time): """Test the Met Office weather platform.""" @@ -85,10 +94,14 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) requests_mock.get( - "/public/data/val/wxfcs/all/json/354107?res=3hourly", text=wavertree_hourly, + "/public/data/val/wxfcs/all/json/354107?res=3hourly", + text=wavertree_hourly, ) - entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,) + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -106,7 +119,8 @@ async def test_one_weather_site_running(hass, requests_mock, legacy_patchable_ti @patch( - "datapoint.Forecast.datetime.datetime", NewDateTime, + "datapoint.Forecast.datetime.datetime", + NewDateTime, ) async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_time): """Test we handle two different weather sites both running.""" @@ -125,10 +139,16 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t "/public/data/val/wxfcs/all/json/322380?res=3hourly", text=kingslynn_hourly ) - entry = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_WAVERTREE,) + entry = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_WAVERTREE, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - entry2 = MockConfigEntry(domain=DOMAIN, data=METOFFICE_CONFIG_KINGSLYNN,) + entry2 = MockConfigEntry( + domain=DOMAIN, + data=METOFFICE_CONFIG_KINGSLYNN, + ) entry2.add_to_hass(hass) await hass.config_entries.async_setup(entry2.entry_id) await hass.async_block_till_done() diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index b3dca7269eb..30df96e89a0 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -16,7 +16,10 @@ async def test_setup_with_no_config(hass): async def test_successful_config_entry(hass): """Test config entry successful setup.""" - entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) + entry = MockConfigEntry( + domain=mikrotik.DOMAIN, + data=MOCK_DATA, + ) entry.add_to_hass(hass) mock_registry = Mock() @@ -50,7 +53,10 @@ async def test_successful_config_entry(hass): async def test_hub_fail_setup(hass): """Test that a failed setup will not store the hub.""" - entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) + entry = MockConfigEntry( + domain=mikrotik.DOMAIN, + data=MOCK_DATA, + ) entry.add_to_hass(hass) with patch.object(mikrotik, "MikrotikHub") as mock_hub: @@ -62,11 +68,15 @@ async def test_hub_fail_setup(hass): async def test_unload_entry(hass): """Test being able to unload an entry.""" - entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) + entry = MockConfigEntry( + domain=mikrotik.DOMAIN, + data=MOCK_DATA, + ) entry.add_to_hass(hass) with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( - "homeassistant.helpers.device_registry.async_get_registry", return_value=Mock(), + "homeassistant.helpers.device_registry.async_get_registry", + return_value=Mock(), ): mock_hub.return_value.async_setup = AsyncMock(return_value=True) mock_hub.return_value.serial_num = "12345678" diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index 54b8dbc9b59..0bd059a1786 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -51,7 +51,9 @@ async def test_flow_entry_already_exists(hass): } first_entry = MockConfigEntry( - domain="mill", data=test_data, unique_id=test_data[CONF_USERNAME], + domain="mill", + data=test_data, + unique_id=test_data[CONF_USERNAME], ) first_entry.add_to_hass(hass) @@ -73,7 +75,9 @@ async def test_connection_error(hass): } first_entry = MockConfigEntry( - domain="mill", data=test_data, unique_id=test_data[CONF_USERNAME], + domain="mill", + data=test_data, + unique_id=test_data[CONF_USERNAME], ) first_entry.add_to_hass(hass) diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 17ec9080e86..2f8ae5ff0bf 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -104,7 +104,8 @@ async def test_invalid_ip(hass: HomeAssistantType) -> None: async def test_same_host(hass: HomeAssistantType) -> None: """Test abort in case of same host name.""" with patch( - "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + "aiodns.DNSResolver.query", + side_effect=aiodns.error.DNSError, ): with patch( "mcstatus.server.MinecraftServer.status", @@ -132,7 +133,8 @@ async def test_same_host(hass: HomeAssistantType) -> None: async def test_port_too_small(hass: HomeAssistantType) -> None: """Test error in case of a too small port.""" with patch( - "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + "aiodns.DNSResolver.query", + side_effect=aiodns.error.DNSError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_SMALL @@ -145,7 +147,8 @@ async def test_port_too_small(hass: HomeAssistantType) -> None: async def test_port_too_large(hass: HomeAssistantType) -> None: """Test error in case of a too large port.""" with patch( - "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + "aiodns.DNSResolver.query", + side_effect=aiodns.error.DNSError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_LARGE @@ -158,7 +161,8 @@ async def test_port_too_large(hass: HomeAssistantType) -> None: async def test_connection_failed(hass: HomeAssistantType) -> None: """Test error in case of a failed connection.""" with patch( - "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + "aiodns.DNSResolver.query", + side_effect=aiodns.error.DNSError, ): with patch("mcstatus.server.MinecraftServer.status", side_effect=OSError): result = await hass.config_entries.flow.async_init( @@ -172,7 +176,8 @@ async def test_connection_failed(hass: HomeAssistantType) -> None: async def test_connection_succeeded_with_srv_record(hass: HomeAssistantType) -> None: """Test config entry in case of a successful connection with a SRV record.""" with patch( - "aiodns.DNSResolver.query", return_value=SRV_RECORDS, + "aiodns.DNSResolver.query", + return_value=SRV_RECORDS, ): with patch( "mcstatus.server.MinecraftServer.status", @@ -191,7 +196,8 @@ async def test_connection_succeeded_with_srv_record(hass: HomeAssistantType) -> async def test_connection_succeeded_with_host(hass: HomeAssistantType) -> None: """Test config entry in case of a successful connection with a host name.""" with patch( - "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + "aiodns.DNSResolver.query", + side_effect=aiodns.error.DNSError, ): with patch( "mcstatus.server.MinecraftServer.status", @@ -211,7 +217,8 @@ async def test_connection_succeeded_with_ip4(hass: HomeAssistantType) -> None: """Test config entry in case of a successful connection with an IPv4 address.""" with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"): with patch( - "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + "aiodns.DNSResolver.query", + side_effect=aiodns.error.DNSError, ): with patch( "mcstatus.server.MinecraftServer.status", @@ -231,7 +238,8 @@ async def test_connection_succeeded_with_ip6(hass: HomeAssistantType) -> None: """Test config entry in case of a successful connection with an IPv6 address.""" with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"): with patch( - "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + "aiodns.DNSResolver.query", + side_effect=aiodns.error.DNSError, ): with patch( "mcstatus.server.MinecraftServer.status", diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index bd38bca535b..bedaa8ce739 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -143,7 +143,9 @@ async def test_webhook_update_registration(webhook_client, authed_api_client): async def test_webhook_handle_get_zones(hass, create_registrations, webhook_client): """Test that we can get zones properly.""" await async_setup_component( - hass, ZONE_DOMAIN, {ZONE_DOMAIN: {}}, + hass, + ZONE_DOMAIN, + {ZONE_DOMAIN: {}}, ) resp = await webhook_client.post( @@ -266,7 +268,8 @@ async def test_webhook_enable_encryption(hass, webhook_client, create_registrati webhook_id = create_registrations[1]["webhook_id"] enable_enc_resp = await webhook_client.post( - f"/api/webhook/{webhook_id}", json={"type": "enable_encryption"}, + f"/api/webhook/{webhook_id}", + json={"type": "enable_encryption"}, ) assert enable_enc_resp.status == 200 @@ -278,7 +281,8 @@ async def test_webhook_enable_encryption(hass, webhook_client, create_registrati key = enable_enc_json["secret"] enc_required_resp = await webhook_client.post( - f"/api/webhook/{webhook_id}", json=RENDER_TEMPLATE, + f"/api/webhook/{webhook_id}", + json=RENDER_TEMPLATE, ) assert enc_required_resp.status == 400 diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 8c6e2a3916c..49a7ed27e5a 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -37,7 +37,8 @@ async def test_form(hass): ), patch( "homeassistant.components.monoprice.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.monoprice.async_setup_entry", return_value=True, + "homeassistant.components.monoprice.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index ccd70c628e2..41a33fd095b 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -97,7 +97,8 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.monoprice.get_monoprice", side_effect=SerialException, + "homeassistant.components.monoprice.get_monoprice", + side_effect=SerialException, ): config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) config_entry.add_to_hass(hass) @@ -108,7 +109,8 @@ async def test_cannot_connect(hass): async def _setup_monoprice(hass, monoprice): with patch( - "homeassistant.components.monoprice.get_monoprice", new=lambda *a: monoprice, + "homeassistant.components.monoprice.get_monoprice", + new=lambda *a: monoprice, ): config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) config_entry.add_to_hass(hass) @@ -118,7 +120,8 @@ async def _setup_monoprice(hass, monoprice): async def _setup_monoprice_with_options(hass, monoprice): with patch( - "homeassistant.components.monoprice.get_monoprice", new=lambda *a: monoprice, + "homeassistant.components.monoprice.get_monoprice", + new=lambda *a: monoprice, ): config_entry = MockConfigEntry( domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS @@ -130,7 +133,8 @@ async def _setup_monoprice_with_options(hass, monoprice): async def _setup_monoprice_not_first_run(hass, monoprice): with patch( - "homeassistant.components.monoprice.get_monoprice", new=lambda *a: monoprice, + "homeassistant.components.monoprice.get_monoprice", + new=lambda *a: monoprice, ): data = {**MOCK_CONFIG, CONF_NOT_FIRST_RUN: True} config_entry = MockConfigEntry(domain=DOMAIN, data=data) diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 59210c63b90..234a8a1e34d 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -54,6 +54,9 @@ async def test_moon_day2(hass): 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, + 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_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 734e1fd552f..30ec5316399 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -105,7 +105,9 @@ async def test_fail_setup_without_command_topic(hass, mqtt_mock): async def test_update_state_via_state_topic(hass, mqtt_mock): """Test updating with via state topic.""" assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -131,7 +133,9 @@ async def test_update_state_via_state_topic(hass, mqtt_mock): async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): """Test ignoring updates via state topic.""" assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -146,7 +150,9 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): async def test_arm_home_publishes_mqtt(hass, mqtt_mock): """Test publishing of MQTT messages while armed.""" assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -162,7 +168,9 @@ async def test_arm_home_not_publishes_mqtt_with_invalid_code_when_req(hass, mqtt When code_arm_required = True """ assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) call_count = mqtt_mock.async_publish.call_count @@ -177,7 +185,11 @@ async def test_arm_home_publishes_mqtt_when_code_not_req(hass, mqtt_mock): """ config = copy.deepcopy(DEFAULT_CONFIG_CODE) config[alarm_control_panel.DOMAIN]["code_arm_required"] = False - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + config, + ) await hass.async_block_till_done() await common.async_alarm_arm_home(hass) @@ -189,7 +201,9 @@ async def test_arm_home_publishes_mqtt_when_code_not_req(hass, mqtt_mock): async def test_arm_away_publishes_mqtt(hass, mqtt_mock): """Test publishing of MQTT messages while armed.""" assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -205,7 +219,9 @@ async def test_arm_away_not_publishes_mqtt_with_invalid_code_when_req(hass, mqtt When code_arm_required = True """ assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) call_count = mqtt_mock.async_publish.call_count @@ -220,7 +236,11 @@ async def test_arm_away_publishes_mqtt_when_code_not_req(hass, mqtt_mock): """ config = copy.deepcopy(DEFAULT_CONFIG_CODE) config[alarm_control_panel.DOMAIN]["code_arm_required"] = False - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + config, + ) await hass.async_block_till_done() await common.async_alarm_arm_away(hass) @@ -232,7 +252,9 @@ async def test_arm_away_publishes_mqtt_when_code_not_req(hass, mqtt_mock): async def test_arm_night_publishes_mqtt(hass, mqtt_mock): """Test publishing of MQTT messages while armed.""" assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -248,7 +270,9 @@ async def test_arm_night_not_publishes_mqtt_with_invalid_code_when_req(hass, mqt When code_arm_required = True """ assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) call_count = mqtt_mock.async_publish.call_count @@ -263,7 +287,11 @@ async def test_arm_night_publishes_mqtt_when_code_not_req(hass, mqtt_mock): """ config = copy.deepcopy(DEFAULT_CONFIG_CODE) config[alarm_control_panel.DOMAIN]["code_arm_required"] = False - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + config, + ) await hass.async_block_till_done() await common.async_alarm_arm_night(hass) @@ -352,7 +380,9 @@ async def test_arm_custom_bypass_publishes_mqtt_when_code_not_req(hass, mqtt_moc async def test_disarm_publishes_mqtt(hass, mqtt_mock): """Test publishing of MQTT messages while disarmed.""" assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) await hass.async_block_till_done() @@ -370,7 +400,11 @@ async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): config[alarm_control_panel.DOMAIN]["command_template"] = ( '{"action":"{{ action }}",' '"code":"{{ code }}"}' ) - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + config, + ) await hass.async_block_till_done() await common.async_alarm_disarm(hass, 1234) @@ -387,7 +421,11 @@ async def test_disarm_publishes_mqtt_when_code_not_req(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG_CODE) config[alarm_control_panel.DOMAIN]["code"] = "1234" config[alarm_control_panel.DOMAIN]["code_disarm_required"] = False - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + config, + ) await hass.async_block_till_done() await common.async_alarm_disarm(hass) @@ -400,7 +438,9 @@ async def test_disarm_not_publishes_mqtt_with_invalid_code_when_req(hass, mqtt_m When code_disarm_required = True """ assert await async_setup_component( - hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE, + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) call_count = mqtt_mock.async_publish.call_count diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 89bfde22d87..547d8adad9e 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -74,7 +74,11 @@ async def help_test_default_availability_payload( # Add availability settings to config config = copy.deepcopy(config) config[domain]["availability_topic"] = "availability-topic" - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component( + hass, + domain, + config, + ) await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") @@ -123,7 +127,11 @@ async def help_test_default_availability_list_payload( {"topic": "availability-topic1"}, {"topic": "availability-topic2"}, ] - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component( + hass, + domain, + config, + ) await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") @@ -185,7 +193,11 @@ async def help_test_default_availability_list_single( {"topic": "availability-topic1"}, ] config[domain]["availability_topic"] = "availability-topic" - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component( + hass, + domain, + config, + ) await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") @@ -214,7 +226,11 @@ async def help_test_custom_availability_payload( config[domain]["availability_topic"] = "availability-topic" config[domain]["payload_available"] = "good" config[domain]["payload_not_available"] = "nogood" - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component( + hass, + domain, + config, + ) await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") @@ -336,7 +352,11 @@ async def help_test_setting_attribute_via_mqtt_json_message( # Add JSON attributes settings to config config = copy.deepcopy(config) config[domain]["json_attributes_topic"] = "attr-topic" - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component( + hass, + domain, + config, + ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }') @@ -354,7 +374,11 @@ async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, con config = copy.deepcopy(config) config[domain]["json_attributes_topic"] = "attr-topic" config[domain]["json_attributes_template"] = "{{ value_json['Timer1'] | tojson }}" - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component( + hass, + domain, + config, + ) await hass.async_block_till_done() async_fire_mqtt_message( @@ -376,7 +400,11 @@ async def help_test_update_with_json_attrs_not_dict( # Add JSON attributes settings to config config = copy.deepcopy(config) config[domain]["json_attributes_topic"] = "attr-topic" - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component( + hass, + domain, + config, + ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "attr-topic", '[ "list", "of", "things"]') @@ -396,7 +424,11 @@ async def help_test_update_with_json_attrs_bad_JSON( # Add JSON attributes settings to config config = copy.deepcopy(config) config[domain]["json_attributes_topic"] = "attr-topic" - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component( + hass, + domain, + config, + ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "attr-topic", "This is not JSON") @@ -670,7 +702,11 @@ async def help_test_entity_id_update_subscriptions( topics = ["avty-topic", "test-topic"] assert len(topics) > 0 registry = mock_registry(hass, {}) - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component( + hass, + domain, + config, + ) await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 5fbb772f949..ef6e7a7656c 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -505,7 +505,8 @@ async def test_options_bad_birth_message_fails(hass, mock_try_connection): assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"birth_topic": "ha_state/online/#"}, + result["flow_id"], + user_input={"birth_topic": "ha_state/online/#"}, ) assert result["type"] == "form" assert result["errors"]["base"] == "bad_birth" @@ -540,7 +541,8 @@ async def test_options_bad_will_message_fails(hass, mock_try_connection): assert result["step_id"] == "options" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"will_topic": "ha_state/offline/#"}, + result["flow_id"], + user_input={"will_topic": "ha_state/offline/#"}, ) assert result["type"] == "form" assert result["errors"]["base"] == "bad_will" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 0dfef17a145..a4d1261daf8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -791,7 +791,8 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): @pytest.mark.parametrize( - "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], + "mqtt_config", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], ) async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test disabling birth message.""" @@ -829,7 +830,8 @@ async def test_default_will_message(hass, mqtt_client_mock, mqtt_mock): @pytest.mark.parametrize( - "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], + "mqtt_config", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], ) async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock): """Test will message.""" @@ -837,7 +839,8 @@ async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock): @pytest.mark.parametrize( - "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], + "mqtt_config", + [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], ) async def test_mqtt_subscribes_topics_on_connect(hass, mqtt_client_mock, mqtt_mock): """Test subscription to topic on connect.""" diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 54292aeeb7b..9a3d9abacc9 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -1006,7 +1006,9 @@ async def test_invalid_values(hass, mqtt_mock): # Bad HS color values async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON",' '"color":{"h":"bad","s":"val"}}', + hass, + "test_light_rgb", + '{"state":"ON",' '"color":{"h":"bad","s":"val"}}', ) # Color should not have changed @@ -1028,7 +1030,9 @@ async def test_invalid_values(hass, mqtt_mock): # Bad XY color values async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON",' '"color":{"x":"bad","y":"val"}}', + hass, + "test_light_rgb", + '{"state":"ON",' '"color":{"x":"bad","y":"val"}}', ) # Color should not have changed diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index ecdedf904d4..87139a0f3ee 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -70,7 +70,8 @@ async def test_state_changed_event_sends_message(hass, mqtt_mock): e_id = "fake.entity" pub_topic = "bar" with patch( - ("homeassistant.core.dt_util.utcnow"), return_value=now, + ("homeassistant.core.dt_util.utcnow"), + return_value=now, ): # Add the eventstream component for publishing events assert await add_eventstream(hass, pub_topic=pub_topic) diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index ed022df0dd7..4d1bf7db683 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -19,11 +19,13 @@ async def test_form_user(hass): assert result["errors"] == {} with patch( - "homeassistant.components.myq.config_flow.pymyq.login", return_value=True, + "homeassistant.components.myq.config_flow.pymyq.login", + return_value=True, ), patch( "homeassistant.components.myq.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.myq.async_setup_entry", return_value=True, + "homeassistant.components.myq.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -46,11 +48,13 @@ async def test_import(hass): await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.myq.config_flow.pymyq.login", return_value=True, + "homeassistant.components.myq.config_flow.pymyq.login", + return_value=True, ), patch( "homeassistant.components.myq.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.myq.async_setup_entry", return_value=True, + "homeassistant.components.myq.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -95,7 +99,8 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.myq.config_flow.pymyq.login", side_effect=MyQError, + "homeassistant.components.myq.config_flow.pymyq.login", + side_effect=MyQError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py index 61e49a98b83..61c19325d57 100644 --- a/tests/components/myq/util.py +++ b/tests/components/myq/util.py @@ -11,7 +11,8 @@ from tests.common import MockConfigEntry, load_fixture async def async_init_integration( - hass: HomeAssistant, skip_setup: bool = False, + hass: HomeAssistant, + skip_setup: bool = False, ) -> MockConfigEntry: """Set up the myq integration in Home Assistant.""" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 1ad2fd196cf..8cee7a8c750 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -127,7 +127,10 @@ async def test_option_flow(hass): } config_entry = MockConfigEntry( - domain=DOMAIN, unique_id=DOMAIN, data=VALID_CONFIG, options={}, + domain=DOMAIN, + unique_id=DOMAIN, + data=VALID_CONFIG, + options={}, ) config_entry.add_to_hass(hass) @@ -182,7 +185,10 @@ async def test_option_flow_wrong_coordinates(hass): } config_entry = MockConfigEntry( - domain=DOMAIN, unique_id=DOMAIN, data=VALID_CONFIG, options={}, + domain=DOMAIN, + unique_id=DOMAIN, + data=VALID_CONFIG, + options={}, ) config_entry.add_to_hass(hass) diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index 0dce512cff4..2d9ca00fbe3 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -26,10 +26,12 @@ async def test_form(hass): ), patch( "homeassistant.components.nexia.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.nexia.async_setup_entry", return_value=True, + "homeassistant.components.nexia.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + result["flow_id"], + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, ) assert result2["type"] == "create_entry" @@ -51,7 +53,8 @@ async def test_form_invalid_auth(hass): with patch("homeassistant.components.nexia.config_flow.NexiaHome.login"): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + result["flow_id"], + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, ) assert result2["type"] == "form" @@ -69,7 +72,8 @@ async def test_form_cannot_connect(hass): side_effect=ConnectTimeout, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + result["flow_id"], + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, ) assert result2["type"] == "form" @@ -89,7 +93,8 @@ async def test_form_import(hass): ), patch( "homeassistant.components.nexia.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.nexia.async_setup_entry", return_value=True, + "homeassistant.components.nexia.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index 2da56d50f37..7d34f0894e0 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -13,7 +13,8 @@ from tests.common import MockConfigEntry, load_fixture async def async_init_integration( - hass: HomeAssistant, skip_setup: bool = False, + hass: HomeAssistant, + skip_setup: bool = False, ) -> MockConfigEntry: """Set up the nexia integration in Home Assistant.""" diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py index de1bb8a2b7c..52064d1a92b 100644 --- a/tests/components/nightscout/__init__.py +++ b/tests/components/nightscout/__init__.py @@ -26,7 +26,10 @@ SERVER_STATUS = ServerStatus.new_from_json_dict( async def init_integration(hass) -> MockConfigEntry: """Set up the Nightscout integration in Home Assistant.""" - entry = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: "https://some.url:1234"}, + ) with patch( "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", return_value=GLUCOSE_READINGS, @@ -43,7 +46,10 @@ async def init_integration(hass) -> MockConfigEntry: async def init_integration_unavailable(hass) -> MockConfigEntry: """Set up the Nightscout integration in Home Assistant.""" - entry = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: "https://some.url:1234"}, + ) with patch( "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", side_effect=ClientConnectionError(), @@ -60,7 +66,10 @@ async def init_integration_unavailable(hass) -> MockConfigEntry: async def init_integration_empty_response(hass) -> MockConfigEntry: """Set up the Nightscout integration in Home Assistant.""" - entry = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "https://some.url:1234"},) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: "https://some.url:1234"}, + ) with patch( "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", return_value=[] ), patch( diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index 99eae99160a..a5f3315fbb1 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -24,7 +24,8 @@ async def test_form(hass): with _patch_glucose_readings(), _patch_server_status(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG, + result["flow_id"], + CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -46,7 +47,8 @@ async def test_user_form_cannot_connect(hass): side_effect=ClientConnectionError(), ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_URL: "https://some.url:1234"}, + result["flow_id"], + {CONF_URL: "https://some.url:1234"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -64,7 +66,8 @@ async def test_user_form_unexpected_exception(hass): side_effect=Exception(), ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_URL: "https://some.url:1234"}, + result["flow_id"], + {CONF_URL: "https://some.url:1234"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -78,7 +81,9 @@ async def test_user_form_duplicate(hass): entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id) await hass.config_entries.async_add(entry) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG, + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONFIG, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -90,7 +95,8 @@ def _patch_async_setup(): def _patch_async_setup_entry(): return patch( - "homeassistant.components.nightscout.async_setup_entry", return_value=True, + "homeassistant.components.nightscout.async_setup_entry", + return_value=True, ) diff --git a/tests/components/nightscout/test_init.py b/tests/components/nightscout/test_init.py index 94953b7e5b2..d81559ceba7 100644 --- a/tests/components/nightscout/test_init.py +++ b/tests/components/nightscout/test_init.py @@ -31,7 +31,8 @@ async def test_unload_entry(hass): async def test_async_setup_raises_entry_not_ready(hass): """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_URL: "https://some.url:1234"}, + domain=DOMAIN, + data={CONF_URL: "https://some.url:1234"}, ) config_entry.add_to_hass(hass) diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index b407461fa89..51e80e6b3c1 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -20,7 +20,8 @@ async def test_climate_thermostat_run(hass): mock_nuheat = _get_mock_nuheat(get_thermostat=mock_thermostat) with patch( - "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + "homeassistant.components.nuheat.nuheat.NuHeat", + return_value=mock_nuheat, ): assert await async_setup_component(hass, DOMAIN, _mock_get_config()) await hass.async_block_till_done() @@ -50,7 +51,8 @@ async def test_climate_thermostat_schedule_hold_unavailable(hass): mock_nuheat = _get_mock_nuheat(get_thermostat=mock_thermostat) with patch( - "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + "homeassistant.components.nuheat.nuheat.NuHeat", + return_value=mock_nuheat, ): assert await async_setup_component(hass, DOMAIN, _mock_get_config()) await hass.async_block_till_done() @@ -77,7 +79,8 @@ async def test_climate_thermostat_schedule_hold_available(hass): mock_nuheat = _get_mock_nuheat(get_thermostat=mock_thermostat) with patch( - "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + "homeassistant.components.nuheat.nuheat.NuHeat", + return_value=mock_nuheat, ): assert await async_setup_component(hass, DOMAIN, _mock_get_config()) await hass.async_block_till_done() @@ -108,7 +111,8 @@ async def test_climate_thermostat_schedule_temporary_hold(hass): mock_nuheat = _get_mock_nuheat(get_thermostat=mock_thermostat) with patch( - "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + "homeassistant.components.nuheat.nuheat.NuHeat", + return_value=mock_nuheat, ): assert await async_setup_component(hass, DOMAIN, _mock_get_config()) await hass.async_block_till_done() diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py index 4a7a8673230..8cd08f2edd5 100644 --- a/tests/components/nuheat/test_init.py +++ b/tests/components/nuheat/test_init.py @@ -17,7 +17,8 @@ async def test_init_success(hass): mock_nuheat = _get_mock_nuheat() with patch( - "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + "homeassistant.components.nuheat.nuheat.NuHeat", + return_value=mock_nuheat, ): assert await async_setup_component(hass, DOMAIN, VALID_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index d86c0d8eb88..8d50d77c31d 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -33,7 +33,8 @@ async def test_form_zeroconf(hass): ) with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -44,11 +45,13 @@ async def test_form_zeroconf(hass): assert result2["type"] == "form" with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ), patch( "homeassistant.components.nut.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.nut.async_setup_entry", return_value=True, + "homeassistant.components.nut.async_setup_entry", + return_value=True, ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -84,7 +87,8 @@ async def test_form_user_one_ups(hass): ) with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -100,11 +104,13 @@ async def test_form_user_one_ups(hass): assert result2["type"] == "form" with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ), patch( "homeassistant.components.nut.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.nut.async_setup_entry", return_value=True, + "homeassistant.components.nut.async_setup_entry", + return_value=True, ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -148,7 +154,8 @@ async def test_form_user_multiple_ups(hass): ) with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -164,24 +171,29 @@ async def test_form_user_multiple_ups(hass): assert result2["type"] == "form" with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ): result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], {"alias": "ups2"}, + result2["flow_id"], + {"alias": "ups2"}, ) assert result3["step_id"] == "resources" assert result3["type"] == "form" with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ), patch( "homeassistant.components.nut.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.nut.async_setup_entry", return_value=True, + "homeassistant.components.nut.async_setup_entry", + return_value=True, ) as mock_setup_entry: result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], {"resources": ["battery.voltage"]}, + result3["flow_id"], + {"resources": ["battery.voltage"]}, ) assert result4["type"] == "create_entry" @@ -209,11 +221,13 @@ async def test_form_import(hass): ) with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ), patch( "homeassistant.components.nut.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.nut.async_setup_entry", return_value=True, + "homeassistant.components.nut.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -280,7 +294,8 @@ async def test_form_cannot_connect(hass): mock_pynut = _get_mock_pynutclient() with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -312,7 +327,8 @@ async def test_options_flow(hass): ) with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ), patch("homeassistant.components.nut.async_setup_entry", return_value=True): result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -330,7 +346,8 @@ async def test_options_flow(hass): } with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ), patch("homeassistant.components.nut.async_setup_entry", return_value=True): result2 = await hass.config_entries.options.async_init(config_entry.entry_id) diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index 5622438d70b..772667b6b50 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -28,7 +28,8 @@ async def async_init_integration( mock_pynut = _get_mock_pynutclient(list_ups={"ups1": "UPS 1"}, list_vars=list_vars) with patch( - "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, ): entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index bca852fa379..72e02f32b9d 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -22,7 +22,8 @@ async def test_form(hass, mock_simple_nws_config): with patch( "homeassistant.components.nws.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.nws.async_setup_entry", return_value=True, + "homeassistant.components.nws.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"api_key": "test"} @@ -51,7 +52,8 @@ async def test_form_cannot_connect(hass, mock_simple_nws_config): ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"api_key": "test"}, + result["flow_id"], + {"api_key": "test"}, ) assert result2["type"] == "form" @@ -68,7 +70,8 @@ async def test_form_unknown_error(hass, mock_simple_nws_config): ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"api_key": "test"}, + result["flow_id"], + {"api_key": "test"}, ) assert result2["type"] == "form" @@ -84,10 +87,12 @@ async def test_form_already_configured(hass, mock_simple_nws_config): with patch( "homeassistant.components.nws.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.nws.async_setup_entry", return_value=True, + "homeassistant.components.nws.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"api_key": "test"}, + result["flow_id"], + {"api_key": "test"}, ) assert result2["type"] == "create_entry" @@ -102,10 +107,12 @@ async def test_form_already_configured(hass, mock_simple_nws_config): with patch( "homeassistant.components.nws.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.nws.async_setup_entry", return_value=True, + "homeassistant.components.nws.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"api_key": "test"}, + result["flow_id"], + {"api_key": "test"}, ) assert result2["type"] == "abort" assert result2["reason"] == "already_configured" diff --git a/tests/components/nws/test_init.py b/tests/components/nws/test_init.py index 68c1e8c8130..9d4acd7dde1 100644 --- a/tests/components/nws/test_init.py +++ b/tests/components/nws/test_init.py @@ -8,7 +8,10 @@ from tests.components.nws.const import NWS_CONFIG async def test_unload_entry(hass, mock_simple_nws): """Test that nws setup with config yaml.""" - entry = MockConfigEntry(domain=DOMAIN, data=NWS_CONFIG,) + entry = MockConfigEntry( + domain=DOMAIN, + data=NWS_CONFIG, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 1486015d80e..06053302ec7 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -35,7 +35,10 @@ async def test_imperial_metric( ): """Test with imperial and metric units.""" hass.config.units = units - entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -73,7 +76,10 @@ async def test_none_values(hass, mock_simple_nws): instance.observation = NONE_OBSERVATION instance.forecast = NONE_FORECAST - entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -95,7 +101,10 @@ async def test_none(hass, mock_simple_nws): instance.observation = None instance.forecast = None - entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -118,7 +127,10 @@ async def test_error_station(hass, mock_simple_nws): instance = mock_simple_nws.return_value instance.set_station.side_effect = aiohttp.ClientError - entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -133,7 +145,10 @@ async def test_entity_refresh(hass, mock_simple_nws): await async_setup_component(hass, "homeassistant", {}) - entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -171,7 +186,10 @@ async def test_error_observation(hass, mock_simple_nws): # first update fails instance.update_observation.side_effect = aiohttp.ClientError - entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -235,7 +253,10 @@ async def test_error_forecast(hass, mock_simple_nws): instance = mock_simple_nws.return_value instance.update_forecast.side_effect = aiohttp.ClientError - entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -263,7 +284,10 @@ async def test_error_forecast_hourly(hass, mock_simple_nws): instance = mock_simple_nws.return_value instance.update_forecast_hourly.side_effect = aiohttp.ClientError - entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry = MockConfigEntry( + domain=nws.DOMAIN, + data=NWS_CONFIG, + ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 5c41349bbbe..864968f848d 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -130,7 +130,12 @@ def setup_mock_device(mock_device): async def setup_onvif_integration( - hass, config=None, options=None, unique_id=MAC, entry_id="1", source="user", + hass, + config=None, + options=None, + unique_id=MAC, + entry_id="1", + source="user", ): """Create an ONVIF config entry.""" if not config: @@ -353,7 +358,8 @@ async def test_flow_manual_entry(hass): setup_mock_device(mock_device) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={}, + result["flow_id"], + user_input={}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 003d2ad7170..a696bc47590 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -26,11 +26,14 @@ async def test_form_user(hass): assert result["errors"] == {} with patch( - "homeassistant.components.opentherm_gw.async_setup", return_value=True, + "homeassistant.components.opentherm_gw.async_setup", + return_value=True, ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, + "pyotgw.pyotgw.connect", + return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, ) as mock_pyotgw_connect, patch( "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: @@ -56,11 +59,14 @@ async def test_form_import(hass): """Test import from existing config.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.opentherm_gw.async_setup", return_value=True, + "homeassistant.components.opentherm_gw.async_setup", + return_value=True, ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, + "pyotgw.pyotgw.connect", + return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, ) as mock_pyotgw_connect, patch( "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: @@ -96,11 +102,14 @@ async def test_form_duplicate_entries(hass): ) with patch( - "homeassistant.components.opentherm_gw.async_setup", return_value=True, + "homeassistant.components.opentherm_gw.async_setup", + return_value=True, ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, + "pyotgw.pyotgw.connect", + return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, ) as mock_pyotgw_connect, patch( "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 06eca531d2d..9c07322acca 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -20,9 +20,11 @@ from tests.common import MockConfigEntry def mock_setup(): """Prevent setup.""" with patch( - "homeassistant.components.openuv.async_setup", return_value=True, + "homeassistant.components.openuv.async_setup", + return_value=True, ), patch( - "homeassistant.components.openuv.async_setup_entry", return_value=True, + "homeassistant.components.openuv.async_setup_entry", + return_value=True, ): yield @@ -58,7 +60,8 @@ async def test_invalid_api_key(hass): } with patch( - "pyopenuv.client.Client.uv_index", side_effect=InvalidApiKeyError, + "pyopenuv.client.Client.uv_index", + side_effect=InvalidApiKeyError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index 73b2610cc7a..b89a9729c35 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -35,7 +35,8 @@ async def test_authorization_error(hass: HomeAssistant) -> None: return_value=False, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT, + result["flow_id"], + FIXTURE_USER_INPUT, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -57,7 +58,8 @@ async def test_connection_error(hass: HomeAssistant) -> None: side_effect=aiohttp.ClientError, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT, + result["flow_id"], + FIXTURE_USER_INPUT, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -79,7 +81,8 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: return_value=True, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT, + result["flow_id"], + FIXTURE_USER_INPUT, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 3e6101e1989..396870c2975 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -50,7 +50,8 @@ def mock_not_supports_encryption(): async def init_config_flow(hass): """Init a configuration flow.""" await async_process_ha_core_config( - hass, {"external_url": BASE_URL}, + hass, + {"external_url": BASE_URL}, ) flow = config_flow.OwnTracksFlow() flow.hass = hass @@ -90,7 +91,8 @@ async def test_import(hass, webhook_id, secret): async def test_import_setup(hass): """Test that we automatically create a config flow.""" await async_process_ha_core_config( - hass, {"external_url": "http://example.com"}, + hass, + {"external_url": "http://example.com"}, ) assert not hass.config_entries.async_entries(DOMAIN) @@ -132,7 +134,8 @@ async def test_user_not_supports_encryption(hass, not_supports_encryption): async def test_unload(hass): """Test unloading a config flow.""" await async_process_ha_core_config( - hass, {"external_url": "http://example.com"}, + hass, + {"external_url": "http://example.com"}, ) with patch( diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index bfe3f922402..3446bbfc7de 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -20,7 +20,8 @@ async def test_user_create_entry(hass): with patch( "homeassistant.components.ozw.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.ozw.async_setup_entry", return_value=True, + "homeassistant.components.ozw.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index 0e7731dbdc0..5359811c52c 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -27,7 +27,8 @@ def panasonic_viera_setup_fixture(): with patch( "homeassistant.components.panasonic_viera.async_setup", return_value=True ), patch( - "homeassistant.components.panasonic_viera.async_setup_entry", return_value=True, + "homeassistant.components.panasonic_viera.async_setup_entry", + return_value=True, ): yield @@ -80,7 +81,8 @@ async def test_flow_non_encrypted(hass): return_value=mock_remote, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, + result["flow_id"], + {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, ) assert result["type"] == "create_entry" @@ -108,7 +110,8 @@ async def test_flow_not_connected_error(hass): side_effect=TimeoutError, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, + result["flow_id"], + {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, ) assert result["type"] == "form" @@ -131,7 +134,8 @@ async def test_flow_unknown_abort(hass): side_effect=Exception, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, + result["flow_id"], + {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, ) assert result["type"] == "abort" @@ -149,7 +153,9 @@ async def test_flow_encrypted_valid_pin_code(hass): assert result["step_id"] == "user" mock_remote = get_mock_remote( - encrypted=True, app_id="test-app-id", encryption_key="test-encryption-key", + encrypted=True, + app_id="test-app-id", + encryption_key="test-encryption-key", ) with patch( @@ -157,14 +163,16 @@ async def test_flow_encrypted_valid_pin_code(hass): return_value=mock_remote, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, + result["flow_id"], + {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, ) assert result["type"] == "form" assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_PIN: "1234"}, + result["flow_id"], + {CONF_PIN: "1234"}, ) assert result["type"] == "create_entry" @@ -196,7 +204,8 @@ async def test_flow_encrypted_invalid_pin_code_error(hass): return_value=mock_remote, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, + result["flow_id"], + {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, ) assert result["type"] == "form" @@ -207,7 +216,8 @@ async def test_flow_encrypted_invalid_pin_code_error(hass): return_value=mock_remote, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_PIN: "0000"}, + result["flow_id"], + {CONF_PIN: "0000"}, ) assert result["type"] == "form" @@ -232,14 +242,16 @@ async def test_flow_encrypted_not_connected_abort(hass): return_value=mock_remote, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, + result["flow_id"], + {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, ) assert result["type"] == "form" assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_PIN: "0000"}, + result["flow_id"], + {CONF_PIN: "0000"}, ) assert result["type"] == "abort" @@ -263,14 +275,16 @@ async def test_flow_encrypted_unknown_abort(hass): return_value=mock_remote, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, + result["flow_id"], + {CONF_HOST: "1.2.3.4", CONF_NAME: DEFAULT_NAME}, ) assert result["type"] == "form" assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_PIN: "0000"}, + result["flow_id"], + {CONF_PIN: "0000"}, ) assert result["type"] == "abort" @@ -355,7 +369,9 @@ async def test_imported_flow_encrypted_valid_pin_code(hass): """Test imported flow with encryption and valid PIN code.""" mock_remote = get_mock_remote( - encrypted=True, app_id="test-app-id", encryption_key="test-encryption-key", + encrypted=True, + app_id="test-app-id", + encryption_key="test-encryption-key", ) with patch( @@ -377,7 +393,8 @@ async def test_imported_flow_encrypted_valid_pin_code(hass): assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_PIN: "1234"}, + result["flow_id"], + {CONF_PIN: "1234"}, ) assert result["type"] == "create_entry" @@ -420,7 +437,8 @@ async def test_imported_flow_encrypted_invalid_pin_code_error(hass): return_value=mock_remote, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_PIN: "0000"}, + result["flow_id"], + {CONF_PIN: "0000"}, ) assert result["type"] == "form" @@ -452,7 +470,8 @@ async def test_imported_flow_encrypted_not_connected_abort(hass): assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_PIN: "0000"}, + result["flow_id"], + {CONF_PIN: "0000"}, ) assert result["type"] == "abort" @@ -483,7 +502,8 @@ async def test_imported_flow_encrypted_unknown_abort(hass): assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_PIN: "0000"}, + result["flow_id"], + {CONF_PIN: "0000"}, ) assert result["type"] == "abort" diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index 327ab5829c1..a4a1ca94fe5 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -52,7 +52,8 @@ async def test_setup_entry_encrypted(hass): mock_remote = get_mock_remote() with patch( - "homeassistant.components.panasonic_viera.Remote", return_value=mock_remote, + "homeassistant.components.panasonic_viera.Remote", + return_value=mock_remote, ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -66,7 +67,9 @@ async def test_setup_entry_encrypted(hass): async def test_setup_entry_unencrypted(hass): """Test setup with unencrypted config entry.""" mock_entry = MockConfigEntry( - domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], data=MOCK_CONFIG_DATA, + domain=DOMAIN, + unique_id=MOCK_CONFIG_DATA[CONF_HOST], + data=MOCK_CONFIG_DATA, ) mock_entry.add_to_hass(hass) @@ -74,7 +77,8 @@ async def test_setup_entry_unencrypted(hass): mock_remote = get_mock_remote() with patch( - "homeassistant.components.panasonic_viera.Remote", return_value=mock_remote, + "homeassistant.components.panasonic_viera.Remote", + return_value=mock_remote, ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -88,7 +92,11 @@ async def test_setup_entry_unencrypted(hass): async def test_setup_config_flow_initiated(hass): """Test if config flow is initiated in setup.""" assert ( - await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_HOST: "0.0.0.0"}},) + await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {CONF_HOST: "0.0.0.0"}}, + ) is True ) @@ -106,7 +114,8 @@ async def test_setup_unload_entry(hass): mock_remote = get_mock_remote() with patch( - "homeassistant.components.panasonic_viera.Remote", return_value=mock_remote, + "homeassistant.components.panasonic_viera.Remote", + return_value=mock_remote, ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 07a9e08313a..714f211d4f8 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -30,7 +30,8 @@ def _flow_next(hass, flow_id): def _patch_setup(): return patch( - "homeassistant.components.pi_hole.async_setup_entry", return_value=True, + "homeassistant.components.pi_hole.async_setup_entry", + return_value=True, ) @@ -70,7 +71,8 @@ async def test_flow_user(hass): mocked_hole = _create_mocked_hole() with _patch_config_flow_hole(mocked_hole), _patch_setup(): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -78,7 +80,8 @@ async def test_flow_user(hass): _flow_next(hass, result["flow_id"]) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=CONF_CONFIG_FLOW, + result["flow_id"], + user_input=CONF_CONFIG_FLOW, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME @@ -86,7 +89,9 @@ async def test_flow_user(hass): # duplicated server result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW, + DOMAIN, + context={"source": SOURCE_USER}, + data=CONF_CONFIG_FLOW, ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index 991eb1d385d..866702488dc 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -71,7 +71,8 @@ async def test_low_battery(hass): sensor.hass = hass assert sensor.state_attributes["problem"] == "none" sensor.state_changed( - "sensor.mqtt_plant_battery", State("sensor.mqtt_plant_battery", 10), + "sensor.mqtt_plant_battery", + State("sensor.mqtt_plant_battery", 10), ) assert sensor.state == "problem" assert sensor.state_attributes["problem"] == "battery low" diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 92124b7b39c..1bd2ce82863 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -45,7 +45,8 @@ from tests.common import MockConfigEntry async def test_bad_credentials(hass): """Test when provided credentials are rejected.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( @@ -79,7 +80,8 @@ async def test_bad_hostname(hass): mock_plex_account = MockPlexAccount() await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( @@ -115,7 +117,8 @@ async def test_bad_hostname(hass): async def test_unknown_exception(hass): """Test when an unknown exception is encountered.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( @@ -144,7 +147,8 @@ async def test_no_servers_found(hass): """Test when no servers are on an account.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( @@ -178,7 +182,8 @@ async def test_single_available_server(hass): mock_plex_server = MockPlexServer() await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( @@ -217,7 +222,8 @@ async def test_multiple_servers_with_selection(hass): mock_plex_server = MockPlexServer() await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( @@ -246,7 +252,8 @@ async def test_multiple_servers_with_selection(hass): assert result["step_id"] == "select_server" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_SERVER: MOCK_SERVERS[0][CONF_SERVER]}, + result["flow_id"], + user_input={CONF_SERVER: MOCK_SERVERS[0][CONF_SERVER]}, ) assert result["type"] == "create_entry" assert result["title"] == mock_plex_server.friendlyName @@ -264,7 +271,8 @@ async def test_adding_last_unconfigured_server(hass): mock_plex_server = MockPlexServer() await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) MockConfigEntry( @@ -311,7 +319,8 @@ async def test_all_available_servers_configured(hass): """Test when all available servers are already configured.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) MockConfigEntry( @@ -503,7 +512,8 @@ async def test_external_timed_out(hass): """Test when external flow times out.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( @@ -532,7 +542,8 @@ async def test_callback_view(hass, aiohttp_client): """Test callback view.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( @@ -559,7 +570,8 @@ async def test_callback_view(hass, aiohttp_client): async def test_manual_config(hass): """Test creating via manual configuration.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) class WrongCertValidaitionException(requests.exceptions.SSLError): @@ -638,7 +650,8 @@ async def test_manual_config(hass): assert result["errors"]["base"] == "host_or_token" with patch( - "plexapi.server.PlexServer", side_effect=requests.exceptions.SSLError, + "plexapi.server.PlexServer", + side_effect=requests.exceptions.SSLError, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MANUAL_SERVER @@ -649,7 +662,8 @@ async def test_manual_config(hass): assert result["errors"]["base"] == "ssl_error" with patch( - "plexapi.server.PlexServer", side_effect=WrongCertValidaitionException, + "plexapi.server.PlexServer", + side_effect=WrongCertValidaitionException, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MANUAL_SERVER diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index fa60f4dd7d2..666a819e8ca 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -30,7 +30,10 @@ async def test_set_config_entry_unique_id(hass): mock_plex_server = MockPlexServer() entry = MockConfigEntry( - domain=const.DOMAIN, data=DEFAULT_DATA, options=DEFAULT_OPTIONS, unique_id=None, + domain=const.DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=None, ) with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index feb695aae81..4d8a78f11c8 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -11,7 +11,9 @@ from tests.async_mock import patch @pytest.fixture(name="mock_smile") def mock_smile(): """Create a Mock Smile for testing exceptions.""" - with patch("homeassistant.components.plugwise.config_flow.Smile",) as smile_mock: + with patch( + "homeassistant.components.plugwise.config_flow.Smile", + ) as smile_mock: smile_mock.PlugwiseError = Smile.PlugwiseError smile_mock.InvalidAuthentication = Smile.InvalidAuthentication smile_mock.ConnectionFailedError = Smile.ConnectionFailedError @@ -32,12 +34,15 @@ async def test_form(hass): "homeassistant.components.plugwise.config_flow.Smile.connect", return_value=True, ), patch( - "homeassistant.components.plugwise.async_setup", return_value=True, + "homeassistant.components.plugwise.async_setup", + return_value=True, ) as mock_setup, patch( - "homeassistant.components.plugwise.async_setup_entry", return_value=True, + "homeassistant.components.plugwise.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1", "password": "test-password"}, + result["flow_id"], + {"host": "1.1.1.1", "password": "test-password"}, ) assert result2["type"] == "create_entry" @@ -60,7 +65,8 @@ async def test_form_invalid_auth(hass, mock_smile): mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1", "password": "test-password"}, + result["flow_id"], + {"host": "1.1.1.1", "password": "test-password"}, ) assert result2["type"] == "form" @@ -77,7 +83,8 @@ async def test_form_cannot_connect(hass, mock_smile): mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1", "password": "test-password"}, + result["flow_id"], + {"host": "1.1.1.1", "password": "test-password"}, ) assert result2["type"] == "form" diff --git a/tests/components/plum_lightpad/test_config_flow.py b/tests/components/plum_lightpad/test_config_flow.py index 9fc32f9872c..7f6196ef9b7 100644 --- a/tests/components/plum_lightpad/test_config_flow.py +++ b/tests/components/plum_lightpad/test_config_flow.py @@ -23,7 +23,8 @@ async def test_form(hass): ), patch( "homeassistant.components.plum_lightpad.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.plum_lightpad.async_setup_entry", return_value=True, + "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"], @@ -99,7 +100,8 @@ async def test_import(hass): ), patch( "homeassistant.components.plum_lightpad.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.plum_lightpad.async_setup_entry", return_value=True, + "homeassistant.components.plum_lightpad.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/plum_lightpad/test_init.py b/tests/components/plum_lightpad/test_init.py index b92dbaf1aa5..139ab786d1d 100644 --- a/tests/components/plum_lightpad/test_init.py +++ b/tests/components/plum_lightpad/test_init.py @@ -23,7 +23,8 @@ async def test_async_setup_imports_from_config(hass: HomeAssistant): with patch( "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" ) as mock_loadCloudData, patch( - "homeassistant.components.plum_lightpad.async_setup_entry", return_value=True, + "homeassistant.components.plum_lightpad.async_setup_entry", + return_value=True, ) as mock_async_setup_entry: result = await async_setup_component( hass, diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index b7499208121..c969ed9c416 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -20,7 +20,8 @@ async def test_show_form(hass): async def test_invalid_credentials(hass): """Test we handle invalid credentials.""" with patch( - "poolsense.PoolSense.test_poolsense_credentials", return_value=False, + "poolsense.PoolSense.test_poolsense_credentials", + return_value=False, ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py index 1cdda033c12..caf7519f598 100644 --- a/tests/components/powerwall/test_binary_sensor.py +++ b/tests/components/powerwall/test_binary_sensor.py @@ -18,7 +18,8 @@ async def test_sensors(hass): "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( - "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall, + "homeassistant.components.powerwall.Powerwall", + return_value=mock_powerwall, ): assert await async_setup_component(hass, DOMAIN, _mock_get_config()) await hass.async_block_till_done() diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index eaf53f0beef..9f72f1900f0 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -28,10 +28,12 @@ async def test_form_source_user(hass): ), patch( "homeassistant.components.powerwall.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.powerwall.async_setup_entry", return_value=True, + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_IP_ADDRESS: "1.2.3.4"}, + result["flow_id"], + {CONF_IP_ADDRESS: "1.2.3.4"}, ) assert result2["type"] == "create_entry" @@ -53,7 +55,8 @@ async def test_form_source_import(hass): ), patch( "homeassistant.components.powerwall.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.powerwall.async_setup_entry", return_value=True, + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -82,7 +85,8 @@ async def test_form_cannot_connect(hass): return_value=mock_powerwall, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_IP_ADDRESS: "1.2.3.4"}, + result["flow_id"], + {CONF_IP_ADDRESS: "1.2.3.4"}, ) assert result2["type"] == "form" @@ -102,7 +106,8 @@ async def test_form_wrong_version(hass): return_value=mock_powerwall, ): result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_IP_ADDRESS: "1.2.3.4"}, + result["flow_id"], + {CONF_IP_ADDRESS: "1.2.3.4"}, ) assert result3["type"] == "form" diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index af2835ea679..d007b664633 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -25,7 +25,8 @@ async def test_sensors(hass): device_registry = await hass.helpers.device_registry.async_get_registry() reg_device = device_registry.async_get_device( - identifiers={("powerwall", "TG0123456789AB_TG9876543210BA")}, connections=set(), + identifiers={("powerwall", "TG0123456789AB_TG9876543210BA")}, + connections=set(), ) assert reg_device.model == "PowerWall 2 (GW1)" assert reg_device.sw_version == "1.45.1" diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index c5e0623de5b..d7754cf1c8f 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -88,7 +88,8 @@ def location_info_fixture(): def ps4_setup_fixture(): """Patch ps4 setup entry.""" with patch( - "homeassistant.components.ps4.async_setup_entry", return_value=True, + "homeassistant.components.ps4.async_setup_entry", + return_value=True, ): yield @@ -329,7 +330,9 @@ async def test_0_pin(hass): """Test Pin with leading '0' is passed correctly.""" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "creds"}, data={}, + DOMAIN, + context={"source": "creds"}, + data={}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" diff --git a/tests/components/push/test_camera.py b/tests/components/push/test_camera.py index 74d975fc57c..644db2b9dd5 100644 --- a/tests/components/push/test_camera.py +++ b/tests/components/push/test_camera.py @@ -12,7 +12,8 @@ from tests.common import async_fire_time_changed async def test_bad_posting(hass, aiohttp_client): """Test that posting to wrong api endpoint fails.""" await async_process_ha_core_config( - hass, {"external_url": "http://example.com"}, + hass, + {"external_url": "http://example.com"}, ) await async_setup_component( @@ -42,7 +43,8 @@ async def test_bad_posting(hass, aiohttp_client): async def test_posting_url(hass, aiohttp_client): """Test that posting to api endpoint works.""" await async_process_ha_core_config( - hass, {"external_url": "http://example.com"}, + hass, + {"external_url": "http://example.com"}, ) await async_setup_component( diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index 6075174fa98..5a68a6e6e7b 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -270,7 +270,9 @@ async def test_button(hass, aioclient_mock, qs_devices): button_pressed = Mock() hass.bus.async_listen_once("qwikswitch.button.@a00002", button_pressed) - listen_mock.queue_response(json={"id": "@a00002", "cmd": "TOGGLE"},) + listen_mock.queue_response( + json={"id": "@a00002", "cmd": "TOGGLE"}, + ) await asyncio.sleep(0.01) await hass.async_block_till_done() button_pressed.assert_called_once() diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 7cc3f272e7a..b107bd1514f 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -39,7 +39,8 @@ async def test_form(hass): ), patch( "homeassistant.components.rachio.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.rachio.async_setup_entry", return_value=True, + "homeassistant.components.rachio.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -76,7 +77,8 @@ async def test_form_invalid_auth(hass): "homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_API_KEY: "api_key"}, + result["flow_id"], + {CONF_API_KEY: "api_key"}, ) assert result2["type"] == "form" @@ -97,7 +99,8 @@ async def test_form_cannot_connect(hass): "homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_API_KEY: "api_key"}, + result["flow_id"], + {CONF_API_KEY: "api_key"}, ) assert result2["type"] == "form" diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 7b27bdf2f39..be4a5fd20fe 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -50,7 +50,8 @@ async def test_invalid_password(hass): flow.context = {"source": SOURCE_USER} with patch( - "regenmaschine.client.Client.load_local", side_effect=RainMachineError, + "regenmaschine.client.Client.load_local", + side_effect=RainMachineError, ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {CONF_PASSWORD: "invalid_credentials"} @@ -83,7 +84,8 @@ async def test_step_import(hass): flow.context = {"source": SOURCE_USER} with patch( - "regenmaschine.client.Client.load_local", return_value=True, + "regenmaschine.client.Client.load_local", + return_value=True, ): result = await flow.async_step_import(import_config=conf) @@ -114,7 +116,8 @@ async def test_step_user(hass): flow.context = {"source": SOURCE_USER} with patch( - "regenmaschine.client.Client.load_local", return_value=True, + "regenmaschine.client.Client.load_local", + return_value=True, ): result = await flow.async_step_user(user_input=conf) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index ef4ba7a2b55..4351239064a 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -631,7 +631,8 @@ class TestRestSensor(unittest.TestCase): value_template.hass = self.hass self.rest.update = Mock( - "rest.RestData.update", side_effect=self.update_side_effect(None, None), + "rest.RestData.update", + side_effect=self.update_side_effect(None, None), ) self.sensor = rest.RestSensor( self.hass, @@ -707,11 +708,16 @@ async def test_reload(hass, requests_mock): assert hass.states.get("sensor.mockrest") yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "rest/configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "rest/configuration.yaml", ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "rest", SERVICE_RELOAD, {}, blocking=True, + "rest", + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 94d440312b4..1eb39f00691 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -21,7 +21,8 @@ async def rfxtrx_fixture(hass): async def _signal_event(packet_id): event = rfxtrx.get_rfx_object(packet_id) await hass.async_add_executor_job( - rfx.event_callback, event, + rfx.event_callback, + event, ) await hass.async_block_till_done() @@ -38,7 +39,9 @@ async def rfxtrx_automatic_fixture(hass, rfxtrx): """Fixture that starts up with automatic additions.""" assert await async_setup_component( - hass, "rfxtrx", {"rfxtrx": {"device": "abcd", "automatic_add": True}}, + hass, + "rfxtrx", + {"rfxtrx": {"device": "abcd", "automatic_add": True}}, ) await hass.async_block_till_done() await hass.async_start() diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index 57723d1ede7..1be0a05fc5e 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -23,7 +23,8 @@ async def test_form(hass): ), patch( "homeassistant.components.ring.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.ring.async_setup_entry", return_value=True, + "homeassistant.components.ring.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 197ebfb8213..75038eef377 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -60,7 +60,8 @@ def two_part_alarm(): "partitions", new_callable=PropertyMock(return_value=partition_mocks), ), patch( - "homeassistant.components.risco.RiscoAPI.get_state", return_value=alarm_mock, + "homeassistant.components.risco.RiscoAPI.get_state", + return_value=alarm_mock, ): yield alarm_mock @@ -70,7 +71,8 @@ async def _setup_risco(hass, options={}): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.risco.RiscoAPI.login", return_value=True, + "homeassistant.components.risco.RiscoAPI.login", + return_value=True, ), patch( "homeassistant.components.risco.RiscoAPI.site_uuid", new_callable=PropertyMock(return_value=TEST_SITE_UUID), @@ -90,7 +92,8 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", side_effect=CannotConnectError, + "homeassistant.components.risco.RiscoAPI.login", + side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) config_entry.add_to_hass(hass) @@ -105,7 +108,8 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", side_effect=UnauthorizedError, + "homeassistant.components.risco.RiscoAPI.login", + side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) config_entry.add_to_hass(hass) diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 46b3bae4c78..689b2e17c28 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -27,7 +27,10 @@ SECOND_ENTITY_ID = "binary_sensor.zone_1" def _zone_mock(): - return MagicMock(triggered=False, bypassed=False,) + return MagicMock( + triggered=False, + bypassed=False, + ) @pytest.fixture @@ -44,9 +47,12 @@ def two_zone_alarm(): ), patch.object( zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") ), patch.object( - alarm_mock, "zones", new_callable=PropertyMock(return_value=zone_mocks), + alarm_mock, + "zones", + new_callable=PropertyMock(return_value=zone_mocks), ), patch( - "homeassistant.components.risco.RiscoAPI.get_state", return_value=alarm_mock, + "homeassistant.components.risco.RiscoAPI.get_state", + return_value=alarm_mock, ): yield alarm_mock @@ -56,7 +62,8 @@ async def _setup_risco(hass): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.risco.RiscoAPI.login", return_value=True, + "homeassistant.components.risco.RiscoAPI.login", + return_value=True, ), patch( "homeassistant.components.risco.RiscoAPI.site_uuid", new_callable=PropertyMock(return_value=TEST_SITE_UUID), @@ -76,7 +83,8 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", side_effect=CannotConnectError, + "homeassistant.components.risco.RiscoAPI.login", + side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) config_entry.add_to_hass(hass) @@ -91,7 +99,8 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", side_effect=UnauthorizedError, + "homeassistant.components.risco.RiscoAPI.login", + side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) config_entry.add_to_hass(hass) diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 3a929c3ed3d..ae6e34e6f60 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -26,7 +26,8 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", return_value=True, + "homeassistant.components.risco.config_flow.RiscoAPI.login", + return_value=True, ), patch( "homeassistant.components.risco.config_flow.RiscoAPI.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), @@ -35,7 +36,8 @@ async def test_form(hass): ) as mock_close, patch( "homeassistant.components.risco.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.risco.async_setup_entry", return_value=True, + "homeassistant.components.risco.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA @@ -110,7 +112,9 @@ async def test_form_exception(hass): async def test_form_already_exists(hass): """Test that a flow with an existing username aborts.""" entry = MockConfigEntry( - domain=DOMAIN, unique_id=TEST_DATA["username"], data=TEST_DATA, + domain=DOMAIN, + unique_id=TEST_DATA["username"], + data=TEST_DATA, ) entry.add_to_hass(hass) @@ -136,7 +140,9 @@ async def test_options_flow(hass): } entry = MockConfigEntry( - domain=DOMAIN, unique_id=TEST_DATA["username"], data=TEST_DATA, + domain=DOMAIN, + unique_id=TEST_DATA["username"], + data=TEST_DATA, ) entry.add_to_hass(hass) @@ -148,7 +154,8 @@ async def test_options_flow(hass): assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input=conf, + result["flow_id"], + user_input=conf, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index b576f385173..935368f03b1 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -160,7 +160,8 @@ def get_no_departures_mock(): async def test_rmvtransport_min_config(hass): """Test minimal rmvtransport configuration.""" with patch( - "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), + "RMVtransport.RMVtransport.get_departures", + return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) is True await hass.async_block_till_done() @@ -180,7 +181,8 @@ async def test_rmvtransport_min_config(hass): async def test_rmvtransport_name_config(hass): """Test custom name configuration.""" with patch( - "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), + "RMVtransport.RMVtransport.get_departures", + return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_NAME) await hass.async_block_till_done() @@ -192,7 +194,8 @@ async def test_rmvtransport_name_config(hass): async def test_rmvtransport_misc_config(hass): """Test misc configuration.""" with patch( - "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), + "RMVtransport.RMVtransport.get_departures", + return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_MISC) await hass.async_block_till_done() @@ -205,7 +208,8 @@ async def test_rmvtransport_misc_config(hass): async def test_rmvtransport_dest_config(hass): """Test destination configuration.""" with patch( - "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), + "RMVtransport.RMVtransport.get_departures", + return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_DEST) await hass.async_block_till_done() diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index a73e1b7d5aa..f2da007b5e0 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -97,11 +97,13 @@ def mock_connection( ) aioclient_mock.post( - re.compile(f"{roku_url}/keypress/.*"), text="OK", + re.compile(f"{roku_url}/keypress/.*"), + text="OK", ) aioclient_mock.post( - re.compile(f"{roku_url}/launch/.*"), text="OK", + re.compile(f"{roku_url}/launch/.*"), + text="OK", ) aioclient_mock.post(f"{roku_url}/search", text="OK") diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 403e25e46c6..58acc7a6bdf 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -70,7 +70,8 @@ async def test_form( with patch( "homeassistant.components.roku.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.roku.async_setup_entry", return_value=True, + "homeassistant.components.roku.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input=user_input @@ -113,7 +114,8 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None: user_input = {CONF_HOST: HOST} with patch( - "homeassistant.components.roku.config_flow.Roku.update", side_effect=Exception, + "homeassistant.components.roku.config_flow.Roku.update", + side_effect=Exception, ) as mock_validate_input: result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input=user_input @@ -136,7 +138,8 @@ async def test_import( with patch( "homeassistant.components.roku.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.roku.async_setup_entry", return_value=True, + "homeassistant.components.roku.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=user_input @@ -161,7 +164,9 @@ async def test_ssdp_cannot_connect( discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -174,10 +179,13 @@ async def test_ssdp_unknown_error( """Test we abort SSDP flow on unknown error.""" discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() with patch( - "homeassistant.components.roku.config_flow.Roku.update", side_effect=Exception, + "homeassistant.components.roku.config_flow.Roku.update", + side_effect=Exception, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT @@ -202,7 +210,8 @@ async def test_ssdp_discovery( with patch( "homeassistant.components.roku.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.roku.async_setup_entry", return_value=True, + "homeassistant.components.roku.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], user_input={} diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index 3a627db72a5..b929a48ee25 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -29,7 +29,8 @@ async def test_unload_config_entry( "homeassistant.components.roku.media_player.async_setup_entry", return_value=True, ), patch( - "homeassistant.components.roku.remote.async_setup_entry", return_value=True, + "homeassistant.components.roku.remote.async_setup_entry", + return_value=True, ): entry = await setup_integration(hass, aioclient_mock) diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 197ad56f415..b2ad3a74235 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -55,10 +55,12 @@ async def test_form(hass): ), patch( "homeassistant.components.roomba.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.roomba.async_setup_entry", return_value=True, + "homeassistant.components.roomba.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], VALID_CONFIG, + result["flow_id"], + VALID_CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -94,7 +96,8 @@ async def test_form_cannot_connect(hass): return_value=mocked_roomba, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], VALID_CONFIG, + result["flow_id"], + VALID_CONFIG, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -115,7 +118,8 @@ async def test_form_import(hass): ), patch( "homeassistant.components.roomba.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.roomba.async_setup_entry", return_value=True, + "homeassistant.components.roomba.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 8b6df6a35fd..aae655fb9c5 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -34,14 +34,16 @@ async def test_form_and_auth(hass): assert result["errors"] == {} with patch("homeassistant.components.roon.config_flow.TIMEOUT", 0,), patch( - "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", 0, + "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", + 0, ), patch( "homeassistant.components.roon.config_flow.RoonApi", return_value=RoonApiMock("good_token"), ), patch( "homeassistant.components.roon.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.roon.async_setup_entry", return_value=True, + "homeassistant.components.roon.async_setup_entry", + return_value=True, ) as mock_setup_entry: await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"} @@ -64,7 +66,8 @@ async def test_form_no_token(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch("homeassistant.components.roon.config_flow.TIMEOUT", 0,), patch( - "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", 0, + "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", + 0, ), patch( "homeassistant.components.roon.config_flow.RoonApi", return_value=RoonApiMock(None), @@ -88,7 +91,8 @@ async def test_form_unknown_exception(hass): ) with patch( - "homeassistant.components.roon.config_flow.RoonApi", side_effect=Exception, + "homeassistant.components.roon.config_flow.RoonApi", + side_effect=Exception, ): await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"} @@ -115,14 +119,16 @@ async def test_form_host_already_exists(hass): assert result["errors"] == {} with patch("homeassistant.components.roon.config_flow.TIMEOUT", 0,), patch( - "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", 0, + "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", + 0, ), patch( "homeassistant.components.roon.config_flow.RoonApi", return_value=RoonApiMock("good_token"), ), patch( "homeassistant.components.roon.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.roon.async_setup_entry", return_value=True, + "homeassistant.components.roon.async_setup_entry", + return_value=True, ) as mock_setup_entry: await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 2673ee56559..0e83255c6e3 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -180,7 +180,8 @@ async def test_user_legacy_not_supported(hass): async def test_user_websocket_not_supported(hass): """Test starting a flow by user for not supported device.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=WebSocketProtocolException("Boom"), @@ -198,7 +199,8 @@ async def test_user_websocket_not_supported(hass): async def test_user_not_successful(hass): """Test starting a flow by user but no connection found.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), @@ -215,7 +217,8 @@ async def test_user_not_successful(hass): async def test_user_not_successful_2(hass): """Test starting a flow by user but no connection found.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=ConnectionFailure("Boom"), @@ -339,7 +342,8 @@ async def test_ssdp_legacy_not_supported(hass): async def test_ssdp_websocket_not_supported(hass): """Test starting a flow from discovery for not supported device.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=WebSocketProtocolException("Boom"), @@ -364,7 +368,8 @@ async def test_ssdp_websocket_not_supported(hass): async def test_ssdp_not_successful(hass): """Test starting a flow from discovery but no device found.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), @@ -390,7 +395,8 @@ async def test_ssdp_not_successful(hass): async def test_ssdp_not_successful_2(hass): """Test starting a flow from discovery but no device found.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=ConnectionFailure("Boom"), @@ -460,7 +466,8 @@ async def test_ssdp_already_configured(hass, remote): async def test_autodetect_websocket(hass, remote, remotews): """Test for send key with autodetection of protocol.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), ), patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remotews: enter = Mock() type(enter).token = PropertyMock(return_value="123456789") @@ -482,7 +489,8 @@ async def test_autodetect_websocket(hass, remote, remotews): async def test_autodetect_websocket_ssl(hass, remote, remotews): """Test for send key with autodetection of protocol.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=[WebSocketProtocolException("Boom"), DEFAULT_MOCK], @@ -552,7 +560,8 @@ async def test_autodetect_legacy(hass, remote): async def test_autodetect_none(hass, remote, remotews): """Test for send key with autodetection of protocol.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + "homeassistant.components.samsungtv.bridge.Remote", + side_effect=OSError("Boom"), ) as remote, patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 840d5ac3f4e..cc9a0f37ec8 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -203,7 +203,9 @@ async def test_setup_websocket_2(hass, mock_now): entity_id = f"{DOMAIN}.fake" entry = MockConfigEntry( - domain=SAMSUNGTV_DOMAIN, data=MOCK_ENTRY_WS, unique_id=entity_id, + domain=SAMSUNGTV_DOMAIN, + data=MOCK_ENTRY_WS, + unique_id=entity_id, ) entry.add_to_hass(hass) diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index 5a38090b5c9..4cda15f7303 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -19,7 +19,8 @@ async def test_form(hass): with patch("sense_energy.ASyncSenseable.authenticate", return_value=True,), patch( "homeassistant.components.sense.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.sense.async_setup_entry", return_value=True, + "homeassistant.components.sense.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 9f060b58355..9ae572123b5 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -34,10 +34,12 @@ async def test_full_user_flow_implementation(hass): with patch("homeassistant.components.sentry.config_flow.Dsn"), patch( "homeassistant.components.sentry.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.sentry.async_setup_entry", return_value=True, + "homeassistant.components.sentry.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"dsn": "http://public@sentry.local/1"}, + result["flow_id"], + {"dsn": "http://public@sentry.local/1"}, ) assert result2["type"] == "create_entry" @@ -69,10 +71,12 @@ async def test_user_flow_bad_dsn(hass): ) with patch( - "homeassistant.components.sentry.config_flow.Dsn", side_effect=BadDsn, + "homeassistant.components.sentry.config_flow.Dsn", + side_effect=BadDsn, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"dsn": "foo"}, + result["flow_id"], + {"dsn": "foo"}, ) assert result2["type"] == RESULT_TYPE_FORM @@ -86,10 +90,12 @@ async def test_user_flow_unkown_exception(hass): ) with patch( - "homeassistant.components.sentry.config_flow.Dsn", side_effect=Exception, + "homeassistant.components.sentry.config_flow.Dsn", + side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"dsn": "foo"}, + result["flow_id"], + {"dsn": "foo"}, ) assert result2["type"] == RESULT_TYPE_FORM @@ -99,7 +105,8 @@ async def test_user_flow_unkown_exception(hass): async def test_options_flow(hass): """Test options config flow.""" entry = MockConfigEntry( - domain=DOMAIN, data={"dsn": "http://public@sentry.local/1"}, + domain=DOMAIN, + data={"dsn": "http://public@sentry.local/1"}, ) entry.add_to_hass(hass) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 8a3b7f48fcf..9acd52b6e8e 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -31,10 +31,12 @@ async def test_form(hass): ), patch( "homeassistant.components.shelly.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.shelly.async_setup_entry", return_value=True, + "homeassistant.components.shelly.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1"}, + result["flow_id"], + {"host": "1.1.1.1"}, ) assert result2["type"] == "create_entry" @@ -60,7 +62,8 @@ async def test_form_auth(hass): return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True}, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1"}, + result["flow_id"], + {"host": "1.1.1.1"}, ) assert result2["type"] == "abort" @@ -78,10 +81,12 @@ async def test_form_errors_get_info(hass, error): ) with patch( - "aioshelly.get_info", side_effect=exc, + "aioshelly.get_info", + side_effect=exc, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1"}, + result["flow_id"], + {"host": "1.1.1.1"}, ) assert result2["type"] == "form" @@ -99,10 +104,12 @@ async def test_form_errors_test_connection(hass, error): ) with patch("aioshelly.get_info", return_value={"auth": False}), patch( - "aioshelly.Device.create", side_effect=exc, + "aioshelly.Device.create", + side_effect=exc, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1"}, + result["flow_id"], + {"host": "1.1.1.1"}, ) assert result2["type"] == "form" @@ -134,9 +141,13 @@ async def test_zeroconf(hass): ), patch( "homeassistant.components.shelly.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.shelly.async_setup_entry", return_value=True, + "homeassistant.components.shelly.async_setup_entry", + return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result2["type"] == "create_entry" assert result2["title"] == "Test name" @@ -169,9 +180,13 @@ async def test_zeroconf_confirm_error(hass, error): assert result["errors"] == {} with patch( - "aioshelly.Device.create", side_effect=exc, + "aioshelly.Device.create", + side_effect=exc, ): - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result2["type"] == "form" assert result2["errors"] == {"base": base_error} @@ -204,7 +219,8 @@ async def test_zeroconf_already_configured(hass): async def test_zeroconf_cannot_connect(hass): """Test we get the form.""" with patch( - "aioshelly.get_info", side_effect=asyncio.TimeoutError, + "aioshelly.get_info", + side_effect=asyncio.TimeoutError, ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index b4239dacfab..407068e6f3e 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -53,7 +53,9 @@ class TestSignalMesssenger(unittest.TestCase): """Test send message.""" message = "Testing Signal Messenger platform :)" mock.register_uri( - "POST", "http://127.0.0.1:8080/v2/send", status_code=201, + "POST", + "http://127.0.0.1:8080/v2/send", + status_code=201, ) mock.register_uri( "GET", @@ -74,7 +76,9 @@ class TestSignalMesssenger(unittest.TestCase): """Test send message.""" message = "Testing Signal Messenger platform with attachment :)" mock.register_uri( - "POST", "http://127.0.0.1:8080/v2/send", status_code=201, + "POST", + "http://127.0.0.1:8080/v2/send", + status_code=201, ) mock.register_uri( "GET", @@ -102,7 +106,9 @@ class TestSignalMesssenger(unittest.TestCase): """Test send message.""" message = "Testing Signal Messenger platform :)" mock.register_uri( - "POST", "http://127.0.0.1:8080/v2/send", status_code=201, + "POST", + "http://127.0.0.1:8080/v2/send", + status_code=201, ) mock.register_uri( "GET", diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index d94c431aa14..86cd0fc384c 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -48,7 +48,8 @@ async def test_invalid_credentials(hass): conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} with patch( - "simplipy.API.login_via_credentials", side_effect=InvalidCredentialsError, + "simplipy.API.login_via_credentials", + side_effect=InvalidCredentialsError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -61,7 +62,10 @@ async def test_options_flow(hass): conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="abcde12345", data=conf, options={CONF_CODE: "1234"}, + domain=DOMAIN, + unique_id="abcde12345", + data=conf, + options={CONF_CODE: "1234"}, ) config_entry.add_to_hass(hass) @@ -217,7 +221,8 @@ async def test_unknown_error(hass): conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} with patch( - "simplipy.API.login_via_credentials", side_effect=SimplipyError, + "simplipy.API.login_via_credentials", + side_effect=SimplipyError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 92e5ebfb7e9..2200dd66490 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -22,7 +22,8 @@ CLIENT_SECRET = "5678" async def test_show_user_form(hass): """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" @@ -32,7 +33,8 @@ async def test_show_user_form(hass): async def test_show_user_host_form(hass): """Test that the host form is served after choosing the local option.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -78,7 +80,8 @@ async def test_connection_error(hass): """Test we show user form on Smappee connection error.""" with patch("pysmappee.api.SmappeeLocalApi.logon", return_value=None): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -127,7 +130,8 @@ async def test_full_user_wrong_mdns(hass): return_value=[{"key": "phase0ActivePower", "value": 0}], ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -166,7 +170,8 @@ async def test_user_device_exists_abort(hass): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -226,14 +231,17 @@ async def test_zeroconf_device_exists_abort(hass): async def test_cloud_device_exists_abort(hass): """Test we abort cloud flow if Smappee Cloud device already configured.""" config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="smappeeCloud", source=SOURCE_USER, + domain=DOMAIN, + unique_id="smappeeCloud", + source=SOURCE_USER, ) config_entry.add_to_hass(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -244,7 +252,9 @@ async def test_cloud_device_exists_abort(hass): async def test_zeroconf_abort_if_cloud_device_exists(hass): """Test we abort zeroconf flow if Smappee Cloud device already configured.""" config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="smappeeCloud", source=SOURCE_USER, + domain=DOMAIN, + unique_id="smappeeCloud", + source=SOURCE_USER, ) config_entry.add_to_hass(hass) @@ -283,7 +293,9 @@ async def test_zeroconf_confirm_abort_if_cloud_device_exists(hass): ) config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="smappeeCloud", source=SOURCE_USER, + domain=DOMAIN, + unique_id="smappeeCloud", + source=SOURCE_USER, ) config_entry.add_to_hass(hass) @@ -309,7 +321,8 @@ async def test_abort_cloud_flow_if_local_device_exists(hass): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_CLOUD} @@ -332,7 +345,8 @@ async def test_full_user_flow(hass, aiohttp_client, aioclient_mock, current_requ ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_CLOUD} @@ -418,14 +432,16 @@ async def test_full_user_local_flow(hass): "homeassistant.components.smappee.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["description_placeholders"] is None result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"environment": ENV_LOCAL}, + result["flow_id"], + {"environment": ENV_LOCAL}, ) assert result["step_id"] == ENV_LOCAL assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/smart_meter_texas/conftest.py b/tests/components/smart_meter_texas/conftest.py index 67e8472ae0b..8b089e5d31b 100644 --- a/tests/components/smart_meter_texas/conftest.py +++ b/tests/components/smart_meter_texas/conftest.py @@ -61,7 +61,8 @@ def mock_connection( auth_endpoint = f"{BASE_ENDPOINT}{AUTH_ENDPOINT}" if not auth_fail and not auth_timeout: aioclient_mock.post( - auth_endpoint, json={"token": "token123"}, + auth_endpoint, + json={"token": "token123"}, ) elif auth_fail: aioclient_mock.post( @@ -73,7 +74,8 @@ def mock_connection( aioclient_mock.post(auth_endpoint, exc=asyncio.TimeoutError) aioclient_mock.post( - f"{BASE_ENDPOINT}{METER_ENDPOINT}", json=load_smt_fixture("meter"), + f"{BASE_ENDPOINT}{METER_ENDPOINT}", + json=load_smt_fixture("meter"), ) aioclient_mock.post(f"{BASE_ENDPOINT}{OD_READ_ENDPOINT}", json={"data": None}) if not bad_reading: @@ -83,7 +85,8 @@ def mock_connection( ) else: aioclient_mock.post( - f"{BASE_ENDPOINT}{LATEST_OD_READ_ENDPOINT}", json={}, + f"{BASE_ENDPOINT}{LATEST_OD_READ_ENDPOINT}", + json={}, ) diff --git a/tests/components/smart_meter_texas/test_config_flow.py b/tests/components/smart_meter_texas/test_config_flow.py index d1e88df8a80..729cb0a90b2 100644 --- a/tests/components/smart_meter_texas/test_config_flow.py +++ b/tests/components/smart_meter_texas/test_config_flow.py @@ -52,10 +52,12 @@ async def test_form_invalid_auth(hass): ) with patch( - "smart_meter_texas.Client.authenticate", side_effect=SmartMeterTexasAuthError, + "smart_meter_texas.Client.authenticate", + side_effect=SmartMeterTexasAuthError, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_LOGIN, + result["flow_id"], + TEST_LOGIN, ) assert result2["type"] == "form" @@ -72,7 +74,8 @@ async def test_form_cannot_connect(hass, side_effect): ) with patch( - "smart_meter_texas.Client.authenticate", side_effect=side_effect, + "smart_meter_texas.Client.authenticate", + side_effect=side_effect, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_LOGIN @@ -89,10 +92,12 @@ async def test_form_unknown_exception(hass): ) with patch( - "smart_meter_texas.Client.authenticate", side_effect=Exception, + "smart_meter_texas.Client.authenticate", + side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_LOGIN, + result["flow_id"], + TEST_LOGIN, ) assert result2["type"] == "form" @@ -108,7 +113,8 @@ async def test_form_duplicate_account(hass): ).add_to_hass(hass) with patch( - "smart_meter_texas.Client.authenticate", return_value=True, + "smart_meter_texas.Client.authenticate", + return_value=True, ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/smarthab/test_config_flow.py b/tests/components/smarthab/test_config_flow.py index 8e6d87a53cc..6303fd44def 100644 --- a/tests/components/smarthab/test_config_flow.py +++ b/tests/components/smarthab/test_config_flow.py @@ -84,7 +84,8 @@ async def test_form_unknown_error(hass): ) with patch( - "pysmarthab.SmartHab.async_login", side_effect=Exception, + "pysmarthab.SmartHab.async_login", + side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 643c084720a..ecab269c1a2 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -78,7 +78,8 @@ async def setup_component(hass, config_file, hass_storage): """Load the SmartThing component.""" hass_storage[STORAGE_KEY] = {"data": config_file, "version": STORAGE_VERSION} await async_process_ha_core_config( - hass, {"external_url": "https://test.local"}, + hass, + {"external_url": "https://test.local"}, ) await async_setup_component(hass, "smartthings", {}) diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 91bba9ab405..b776959ea5b 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -100,7 +100,10 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ assert result["data"][CONF_CLIENT_SECRET] == app_oauth_client.client_secret assert result["data"][CONF_CLIENT_ID] == app_oauth_client.client_id assert result["title"] == location.name - entry = next((entry for entry in hass.config_entries.async_entries(DOMAIN)), None,) + entry = next( + (entry for entry in hass.config_entries.async_entries(DOMAIN)), + None, + ) assert entry.unique_id == smartapp.format_unique_id( app.app_id, location.location_id ) @@ -168,7 +171,10 @@ async def test_entry_created_from_update_event( assert result["data"][CONF_CLIENT_SECRET] == app_oauth_client.client_secret assert result["data"][CONF_CLIENT_ID] == app_oauth_client.client_id assert result["title"] == location.name - entry = next((entry for entry in hass.config_entries.async_entries(DOMAIN)), None,) + entry = next( + (entry for entry in hass.config_entries.async_entries(DOMAIN)), + None, + ) assert entry.unique_id == smartapp.format_unique_id( app.app_id, location.location_id ) @@ -236,7 +242,10 @@ async def test_entry_created_existing_app_new_oauth_client( assert result["data"][CONF_CLIENT_SECRET] == app_oauth_client.client_secret assert result["data"][CONF_CLIENT_ID] == app_oauth_client.client_id assert result["title"] == location.name - entry = next((entry for entry in hass.config_entries.async_entries(DOMAIN)), None,) + entry = next( + (entry for entry in hass.config_entries.async_entries(DOMAIN)), + None, + ) assert entry.unique_id == smartapp.format_unique_id( app.app_id, location.location_id ) @@ -409,7 +418,8 @@ async def test_entry_created_with_cloudhook( assert result["data"][CONF_CLIENT_ID] == app_oauth_client.client_id assert result["title"] == location.name entry = next( - (entry for entry in hass.config_entries.async_entries(DOMAIN)), None, + (entry for entry in hass.config_entries.async_entries(DOMAIN)), + None, ) assert entry.unique_id == smartapp.format_unique_id( app.app_id, location.location_id @@ -420,7 +430,8 @@ async def test_invalid_webhook_aborts(hass): """Test flow aborts if webhook is invalid.""" # Webhook confirmation shown await async_process_ha_core_config( - hass, {"external_url": "http://example.local:8123"}, + hass, + {"external_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 014b9a6673c..931e895cb65 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -118,7 +118,8 @@ async def test_base_url_no_longer_https_does_not_load( ): """Test base_url no longer valid creates a new flow.""" await async_process_ha_core_config( - hass, {"external_url": "http://example.local:8123"}, + hass, + {"external_url": "http://example.local:8123"}, ) config_entry.add_to_hass(hass) smartthings_mock.app.return_value = app diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 2e0ad01e552..050dc663487 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -199,7 +199,9 @@ async def test_refresh_weather_forecast_exception() -> None: with patch.object( hass.helpers.event, "async_call_later" ) as call_later, patch.object( - weather, "get_weather_forecast", side_effect=SmhiForecastException(), + weather, + "get_weather_forecast", + side_effect=SmhiForecastException(), ): await weather.async_update() assert len(call_later.mock_calls) == 1 diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 1474b8e13e7..9cf0e2932ec 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -28,7 +28,8 @@ async def test_form(hass): ), patch( "homeassistant.components.solarlog.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.solarlog.async_setup_entry", return_value=True, + "homeassistant.components.solarlog.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": HOST, "name": NAME} diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index 6a6f3f37283..b076f39d0d2 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -55,19 +55,28 @@ def mock_connection( """Mock Sonarr connection.""" if error: mock_connection_error( - aioclient_mock, host=host, port=port, base_path=base_path, + aioclient_mock, + host=host, + port=port, + base_path=base_path, ) return if invalid_auth: mock_connection_invalid_auth( - aioclient_mock, host=host, port=port, base_path=base_path, + aioclient_mock, + host=host, + port=port, + base_path=base_path, ) return if server_error: mock_connection_server_error( - aioclient_mock, host=host, port=port, base_path=base_path, + aioclient_mock, + host=host, + port=port, + base_path=base_path, ) return @@ -232,5 +241,6 @@ def _patch_async_setup(return_value=True): def _patch_async_setup_entry(return_value=True): """Patch the async entry setup of sonarr.""" return patch( - "homeassistant.components.sonarr.async_setup_entry", return_value=return_value, + "homeassistant.components.sonarr.async_setup_entry", + return_value=return_value, ) diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index e376cda04ee..15bd13b580d 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -32,7 +32,8 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_show_user_form(hass: HomeAssistantType) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, ) assert result["step_id"] == "user" @@ -47,7 +48,9 @@ async def test_cannot_connect( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_FORM @@ -63,7 +66,9 @@ async def test_invalid_auth( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_FORM @@ -81,7 +86,9 @@ async def test_unknown_error( side_effect=Exception, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=user_input, ) assert result["type"] == RESULT_TYPE_ABORT @@ -98,7 +105,9 @@ async def test_full_import_flow_implementation( with _patch_async_setup(), _patch_async_setup_entry(): result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=user_input, + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=user_input, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY @@ -115,7 +124,8 @@ async def test_full_user_flow_implementation( mock_connection(aioclient_mock) result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, ) assert result["type"] == RESULT_TYPE_FORM @@ -125,7 +135,8 @@ async def test_full_user_flow_implementation( with _patch_async_setup(), _patch_async_setup_entry(): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=user_input, + result["flow_id"], + user_input=user_input, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY @@ -155,7 +166,8 @@ async def test_full_user_flow_advanced_options( with _patch_async_setup(), _patch_async_setup_entry(): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=user_input, + result["flow_id"], + user_input=user_input, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py index 27361382e78..e9f01290461 100644 --- a/tests/components/sonarr/test_init.py +++ b/tests/components/sonarr/test_init.py @@ -25,7 +25,8 @@ async def test_unload_config_entry( ) -> None: """Test the configuration entry unloading.""" with patch( - "homeassistant.components.sonarr.sensor.async_setup_entry", return_value=True, + "homeassistant.components.sonarr.sensor.async_setup_entry", + return_value=True, ): entry = await setup_integration(hass, aioclient_mock) diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index e837ed0e032..155801dca03 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -49,14 +49,17 @@ def _flow_next(hass, flow_id): def _patch_setup(): return patch( - "homeassistant.components.songpal.async_setup_entry", return_value=True, + "homeassistant.components.songpal.async_setup_entry", + return_value=True, ) async def test_flow_ssdp(hass): """Test working ssdp flow.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DATA, + DOMAIN, + context={"source": SOURCE_SSDP}, + data=SSDP_DATA, ) assert result["type"] == "form" assert result["step_id"] == "init" @@ -82,7 +85,8 @@ async def test_flow_user(hass): with _patch_config_flow_device(mocked_device), _patch_setup(): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -90,7 +94,8 @@ async def test_flow_user(hass): _flow_next(hass, result["flow_id"]) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_ENDPOINT: ENDPOINT}, + result["flow_id"], + user_input={CONF_ENDPOINT: ENDPOINT}, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == MODEL @@ -136,9 +141,11 @@ async def test_flow_import_without_name(hass): def _create_mock_config_entry(hass): - MockConfigEntry(domain=DOMAIN, unique_id="uuid:0000", data=CONF_DATA,).add_to_hass( - hass - ) + MockConfigEntry( + domain=DOMAIN, + unique_id="uuid:0000", + data=CONF_DATA, + ).add_to_hass(hass) async def test_ssdp_bravia(hass): @@ -148,7 +155,9 @@ async def test_ssdp_bravia(hass): "X_ScalarWebAPI_ServiceType" ].append("videoScreen") result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_SSDP}, data=ssdp_data, + DOMAIN, + context={"source": SOURCE_SSDP}, + data=ssdp_data, ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "not_songpal_device" @@ -158,7 +167,9 @@ async def test_sddp_exist(hass): """Test discovering existed device.""" _create_mock_config_entry(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DATA, + DOMAIN, + context={"source": SOURCE_SSDP}, + data=SSDP_DATA, ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 54d54e6ed5b..dd83aefba81 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -50,7 +50,8 @@ async def test_device_registry(hass, config_entry, config, soco): device_registry = await hass.helpers.device_registry.async_get_registry() reg_device = device_registry.async_get_device( - identifiers={("sonos", "RINCON_test")}, connections=set(), + identifiers={("sonos", "RINCON_test")}, + connections=set(), ) assert reg_device.model == "Model Name" assert reg_device.sw_version == "49.2-64250" diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index d351df32101..85089854c4d 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -503,7 +503,10 @@ async def test_should_turn_off( assert mocked_volume.call_count == 2 await hass.services.async_call( - "media_player", "turn_off", {"entity_id": "media_player.soundtouch_1"}, True, + "media_player", + "turn_off", + {"entity_id": "media_player.soundtouch_1"}, + True, ) assert mocked_status.call_count == 3 assert mocked_power_off.call_count == 1 @@ -522,7 +525,10 @@ async def test_should_turn_on( assert mocked_volume.call_count == 2 await hass.services.async_call( - "media_player", "turn_on", {"entity_id": "media_player.soundtouch_1"}, True, + "media_player", + "turn_on", + {"entity_id": "media_player.soundtouch_1"}, + True, ) assert mocked_status.call_count == 3 assert mocked_power_on.call_count == 1 @@ -540,7 +546,10 @@ async def test_volume_up( assert mocked_volume.call_count == 2 await hass.services.async_call( - "media_player", "volume_up", {"entity_id": "media_player.soundtouch_1"}, True, + "media_player", + "volume_up", + {"entity_id": "media_player.soundtouch_1"}, + True, ) assert mocked_volume.call_count == 3 assert mocked_volume_up.call_count == 1 @@ -558,7 +567,10 @@ async def test_volume_down( assert mocked_volume.call_count == 2 await hass.services.async_call( - "media_player", "volume_down", {"entity_id": "media_player.soundtouch_1"}, True, + "media_player", + "volume_down", + {"entity_id": "media_player.soundtouch_1"}, + True, ) assert mocked_volume.call_count == 3 assert mocked_volume_down.call_count == 1 @@ -614,7 +626,10 @@ async def test_play(mocked_play, mocked_status, mocked_volume, hass, one_device) assert mocked_volume.call_count == 2 await hass.services.async_call( - "media_player", "media_play", {"entity_id": "media_player.soundtouch_1"}, True, + "media_player", + "media_play", + {"entity_id": "media_player.soundtouch_1"}, + True, ) assert mocked_status.call_count == 3 assert mocked_play.call_count == 1 @@ -630,7 +645,10 @@ async def test_pause(mocked_pause, mocked_status, mocked_volume, hass, one_devic assert mocked_volume.call_count == 2 await hass.services.async_call( - "media_player", "media_pause", {"entity_id": "media_player.soundtouch_1"}, True, + "media_player", + "media_pause", + {"entity_id": "media_player.soundtouch_1"}, + True, ) assert mocked_status.call_count == 3 assert mocked_pause.call_count == 1 @@ -955,7 +973,11 @@ async def test_remove_zone_slave( @patch("libsoundtouch.device.SoundTouchDevice.add_zone_slave") async def test_add_zone_slave( - mocked_add_zone_slave, mocked_status, mocked_volume, hass, two_zones, + mocked_add_zone_slave, + mocked_status, + mocked_volume, + hass, + two_zones, ): """Test removing a slave from a zone.""" mocked_device = two_zones @@ -998,7 +1020,11 @@ async def test_add_zone_slave( @patch("libsoundtouch.device.SoundTouchDevice.create_zone") async def test_zone_attributes( - mocked_create_zone, mocked_status, mocked_volume, hass, two_zones, + mocked_create_zone, + mocked_status, + mocked_volume, + hass, + two_zones, ): """Test play everywhere.""" mocked_device = two_zones diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index 943da319aef..cfd79fb38f8 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -25,7 +25,8 @@ from tests.common import MockConfigEntry def mock_setup(): """Mock entry setup.""" with patch( - "homeassistant.components.speedtestdotnet.async_setup_entry", return_value=True, + "homeassistant.components.speedtestdotnet.async_setup_entry", + return_value=True, ): yield @@ -88,7 +89,12 @@ async def test_import_success(hass, mock_setup): async def test_options(hass): """Test updating options.""" - entry = MockConfigEntry(domain=DOMAIN, title="SpeedTest", data={}, options={},) + entry = MockConfigEntry( + domain=DOMAIN, + title="SpeedTest", + data={}, + options={}, + ) entry.add_to_hass(hass) with patch("speedtest.Speedtest") as mock_api: @@ -119,7 +125,11 @@ async def test_options(hass): async def test_integration_already_configured(hass): """Test integration is already configured.""" - entry = MockConfigEntry(domain=DOMAIN, data={}, options={},) + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": "user"} diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 7b7eed67c0c..cadf97fc761 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -25,7 +25,10 @@ async def test_setup_with_config(hass): async def test_successful_config_entry(hass): """Test that SpeedTestDotNet is configured successfully.""" - entry = MockConfigEntry(domain=speedtestdotnet.DOMAIN, data={},) + entry = MockConfigEntry( + domain=speedtestdotnet.DOMAIN, + data={}, + ) entry.add_to_hass(hass) with patch("speedtest.Speedtest"), patch( @@ -35,13 +38,19 @@ async def test_successful_config_entry(hass): await hass.config_entries.async_setup(entry.entry_id) assert entry.state == config_entries.ENTRY_STATE_LOADED - assert forward_entry_setup.mock_calls[0][1] == (entry, "sensor",) + assert forward_entry_setup.mock_calls[0][1] == ( + entry, + "sensor", + ) async def test_setup_failed(hass): """Test SpeedTestDotNet failed due to an error.""" - entry = MockConfigEntry(domain=speedtestdotnet.DOMAIN, data={},) + entry = MockConfigEntry( + domain=speedtestdotnet.DOMAIN, + data={}, + ) entry.add_to_hass(hass) with patch("speedtest.Speedtest", side_effect=speedtest.ConfigRetrievalError): @@ -53,7 +62,10 @@ async def test_setup_failed(hass): async def test_unload_entry(hass): """Test removing SpeedTestDotNet.""" - entry = MockConfigEntry(domain=speedtestdotnet.DOMAIN, data={},) + entry = MockConfigEntry( + domain=speedtestdotnet.DOMAIN, + data={}, + ) entry.add_to_hass(hass) with patch("speedtest.Speedtest"): diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index 5c2c074027f..3bf7dd790b2 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -58,9 +58,11 @@ async def test_import(hass, spider): """Test import step.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.spider.async_setup", return_value=True, + "homeassistant.components.spider.async_setup", + return_value=True, ) as mock_setup, patch( - "homeassistant.components.spider.async_setup_entry", return_value=True, + "homeassistant.components.spider.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index e38188e4a0f..f325024cf00 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -46,7 +46,8 @@ async def test_user_form(hass): with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch( "homeassistant.components.squeezebox.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.squeezebox.async_setup_entry", return_value=True, + "homeassistant.components.squeezebox.async_setup_entry", + return_value=True, ) as mock_setup_entry, patch( "homeassistant.components.squeezebox.config_flow.async_discover", mock_discover ): @@ -106,11 +107,13 @@ async def test_user_form_timeout(hass): async def test_user_form_duplicate(hass): """Test duplicate discovered servers are skipped.""" with patch( - "homeassistant.components.squeezebox.config_flow.async_discover", mock_discover, + "homeassistant.components.squeezebox.config_flow.async_discover", + mock_discover, ), patch("homeassistant.components.squeezebox.config_flow.TIMEOUT", 0.1), patch( "homeassistant.components.squeezebox.async_setup", return_value=True ), patch( - "homeassistant.components.squeezebox.async_setup_entry", return_value=True, + "homeassistant.components.squeezebox.async_setup_entry", + return_value=True, ): entry = MockConfigEntry(domain=DOMAIN, unique_id=UUID) await hass.config_entries.async_add(entry) @@ -153,7 +156,8 @@ async def test_form_cannot_connect(hass): ) with patch( - "pysqueezebox.Server.async_query", return_value=False, + "pysqueezebox.Server.async_query", + return_value=False, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -172,7 +176,8 @@ async def test_form_cannot_connect(hass): async def test_discovery(hass): """Test handling of discovered server.""" with patch( - "pysqueezebox.Server.async_query", return_value={"uuid": UUID}, + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -200,7 +205,8 @@ async def test_import(hass): with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch( "homeassistant.components.squeezebox.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.squeezebox.async_setup_entry", return_value=True, + "homeassistant.components.squeezebox.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -248,9 +254,11 @@ async def test_import_existing(hass): with patch( "homeassistant.components.squeezebox.async_setup", return_value=True ), patch( - "homeassistant.components.squeezebox.async_setup_entry", return_value=True, + "homeassistant.components.squeezebox.async_setup_entry", + return_value=True, ), patch( - "pysqueezebox.Server.async_query", return_value={"ip": HOST, "uuid": UUID}, + "pysqueezebox.Server.async_query", + return_value={"ip": HOST, "uuid": UUID}, ): entry = MockConfigEntry(domain=DOMAIN, unique_id=UUID) await hass.config_entries.async_add(entry) diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 7a96ec4e2a4..e60c5c2e9a5 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -479,11 +479,16 @@ async def test_reload(hass): assert hass.states.get("sensor.test") yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "statistics/configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "statistics/configuration.yaml", ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, {}, blocking=True, + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 8eb72ee264a..1df377ff0e8 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -58,7 +58,9 @@ async def test_already_configured_by_url(hass, aioclient_mock): mock_connection(aioclient_mock) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT, + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index f592ad90a88..64527c70964 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -421,7 +421,8 @@ async def test_options_flow(hass: HomeAssistantType, service: MagicMock): # Scan interval # Default result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={}, + result["flow_id"], + user_input={}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL @@ -429,7 +430,8 @@ async def test_options_flow(hass: HomeAssistantType, service: MagicMock): # Manual result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2}, + result["flow_id"], + user_input={CONF_SCAN_INTERVAL: 2}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index 2e42a2bc1fb..9bd3db5e46b 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -30,11 +30,13 @@ async def test_form(hass): mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) with patch( - "homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api, + "homeassistant.components.tado.config_flow.Tado", + return_value=mock_tado_api, ), patch( "homeassistant.components.tado.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.tado.async_setup_entry", return_value=True, + "homeassistant.components.tado.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -59,11 +61,13 @@ async def test_import(hass): mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) with patch( - "homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api, + "homeassistant.components.tado.config_flow.Tado", + return_value=mock_tado_api, ), patch( "homeassistant.components.tado.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.tado.async_setup_entry", return_value=True, + "homeassistant.components.tado.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -93,7 +97,8 @@ async def test_form_invalid_auth(hass): mock_tado_api = _get_mock_tado_api(getMe=requests.HTTPError(response=response_mock)) with patch( - "homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api, + "homeassistant.components.tado.config_flow.Tado", + return_value=mock_tado_api, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -115,7 +120,8 @@ async def test_form_cannot_connect(hass): mock_tado_api = _get_mock_tado_api(getMe=requests.HTTPError(response=response_mock)) with patch( - "homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api, + "homeassistant.components.tado.config_flow.Tado", + return_value=mock_tado_api, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -135,7 +141,8 @@ async def test_no_homes(hass): mock_tado_api = _get_mock_tado_api(getMe={"homes": []}) with patch( - "homeassistant.components.tado.config_flow.Tado", return_value=mock_tado_api, + "homeassistant.components.tado.config_flow.Tado", + return_value=mock_tado_api, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index 5c060e76eec..7dc6a21faa7 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -10,7 +10,8 @@ from tests.common import MockConfigEntry, load_fixture async def async_init_integration( - hass: HomeAssistant, skip_setup: bool = False, + hass: HomeAssistant, + skip_setup: bool = False, ): """Set up the tado integration in Home Assistant.""" @@ -42,7 +43,8 @@ async def async_init_integration( with requests_mock.mock() as m: m.post("https://auth.tado.com/oauth/token", text=load_fixture(token_fixture)) m.get( - "https://my.tado.com/api/v2/me", text=load_fixture(me_fixture), + "https://my.tado.com/api/v2/me", + text=load_fixture(me_fixture), ) m.get( "https://my.tado.com/api/v2/homes/1/devices", diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index c6453ba31ac..d8d4b8fdcf2 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -346,7 +346,9 @@ async def test_invalid_panel_does_not_create(hass, caplog): async def test_no_panels_does_not_create(hass, caplog): """Test if there are no panels -> no creation.""" await setup.async_setup_component( - hass, "alarm_control_panel", {"alarm_control_panel": {"platform": "template"}}, + hass, + "alarm_control_panel", + {"alarm_control_panel": {"platform": "template"}}, ) await hass.async_block_till_done() diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index e655fd72987..2d14ec97574 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -34,11 +34,16 @@ async def test_reloadable(hass): assert len(hass.states.async_all()) == 2 yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "template/sensor_configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "template/sensor_configuration.yaml", ) with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, {}, blocking=True, + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() @@ -76,11 +81,16 @@ async def test_reloadable_can_remove(hass): assert len(hass.states.async_all()) == 2 yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "template/empty_configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "template/empty_configuration.yaml", ) with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, {}, blocking=True, + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() @@ -115,11 +125,16 @@ async def test_reloadable_stops_on_invalid_config(hass): assert len(hass.states.async_all()) == 2 yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "template/configuration.yaml.corrupt", + _get_fixtures_base_path(), + "fixtures", + "template/configuration.yaml.corrupt", ) with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, {}, blocking=True, + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() @@ -155,11 +170,16 @@ async def test_reloadable_handles_partial_valid_config(hass): assert len(hass.states.async_all()) == 2 yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "template/broken_configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "template/broken_configuration.yaml", ) with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, {}, blocking=True, + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() @@ -213,11 +233,16 @@ async def test_reloadable_multiple_platforms(hass): assert len(hass.states.async_all()) == 3 yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "template/sensor_configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "template/sensor_configuration.yaml", ) with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, {}, blocking=True, + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 2b4a34e8c4a..31b298330e8 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -472,7 +472,8 @@ async def test_creating_sensor_loads_group(hass): hass.bus.async_listen(EVENT_COMPONENT_LOADED, set_after_dep_event) with patch( - "homeassistant.components.group.async_setup", new=async_setup_group, + "homeassistant.components.group.async_setup", + new=async_setup_group, ), patch( "homeassistant.components.template.sensor.async_setup_platform", new=async_setup_template, diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 191e26a4266..6dab2569e59 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -541,7 +541,11 @@ async def test_off_action_optimistic(hass, calls): async def test_restore_state(hass): """Test state restoration.""" mock_restore_cache( - hass, (State("switch.s1", STATE_ON), State("switch.s2", STATE_OFF),), + hass, + ( + State("switch.s1", STATE_ON), + State("switch.s2", STATE_OFF), + ), ) hass.state = CoreState.starting diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py index 6b2428fcff9..c92ec5bc5c7 100644 --- a/tests/components/tibber/test_config_flow.py +++ b/tests/components/tibber/test_config_flow.py @@ -52,7 +52,9 @@ async def test_create_entry(hass): async def test_flow_entry_already_exists(hass): """Test user input for config_entry that already exists.""" first_entry = MockConfigEntry( - domain="tibber", data={CONF_ACCESS_TOKEN: "valid"}, unique_id="tibber", + domain="tibber", + data={CONF_ACCESS_TOKEN: "valid"}, + unique_id="tibber", ) first_entry.add_to_hass(hass) diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index 7b9a80b427d..a1812d34a57 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -37,7 +37,8 @@ async def test_invalid_credentials(hass): } with patch( - "homeassistant.components.tile.config_flow.async_login", side_effect=TileError, + "homeassistant.components.tile.config_flow.async_login", + side_effect=TileError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 37e174b6dc4..6fb7a7b53dc 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -16,7 +16,8 @@ from tests.common import MockConfigEntry async def setup_component(hass): """Set up Toon component.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) with patch("os.path.isfile", return_value=False): diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 290151b10cc..8dbe7481339 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -68,7 +68,8 @@ async def test_configuring_device_types(hass, name, cls, platform, count): ) as discover, patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( - "homeassistant.components.tplink.light.async_setup_entry", return_value=True, + "homeassistant.components.tplink.light.async_setup_entry", + return_value=True, ): discovery_data = { f"123.123.123.{c}": cls("123.123.123.123") for c in range(count) diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 241789270d7..cc9ecff896f 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -237,7 +237,10 @@ def dimmer_switch_mock_data_fixture() -> None: async def update_entity(hass: HomeAssistant, entity_id: str) -> None: """Run an update action for an entity.""" await hass.services.async_call( - HA_DOMAIN, SERVICE_UPDATE_ENTITY, {ATTR_ENTITY_ID: entity_id}, blocking=True, + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) await hass.async_block_till_done() @@ -321,7 +324,10 @@ async def test_smartswitch( assert state.state == "off" await hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.dimmer1"}, blocking=True, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.dimmer1"}, + blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.dimmer1") @@ -355,7 +361,10 @@ async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> Non assert hass.states.get("light.light1") await hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") @@ -408,7 +417,10 @@ async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> Non light_state["dft_on_state"]["saturation"] = 78 await hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") @@ -417,7 +429,10 @@ async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> Non assert state.state == "off" await hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") @@ -504,7 +519,10 @@ async def test_get_light_state_retry( await hass.async_block_till_done() await hass.services.async_call( - LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, ) await hass.async_block_till_done() await update_entity(hass, "light.light1") diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 942fd4b01d5..12e1ff5562a 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -61,7 +61,8 @@ async def setup_zones(loop, hass): async def webhook_id_fixture(hass, client): """Initialize the Traccar component and get the webhook_id.""" await async_process_ha_core_config( - hass, {"external_url": "http://example.com"}, + hass, + {"external_url": "http://example.com"}, ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"} diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 43e33706bf6..c9f72b7a9df 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -272,7 +272,10 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): async def test_discovery_updates_unique_id(hass): """Test a duplicate discovery host aborts and updates existing entry.""" - entry = MockConfigEntry(domain="tradfri", data={"host": "some-host"},) + entry = MockConfigEntry( + domain="tradfri", + data={"host": "some-host"}, + ) entry.add_to_hass(hass) flow = await hass.config_entries.flow.async_init( diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index a100ef22c65..d5f099175b1 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -87,7 +87,8 @@ def mutagen_mock(): async def internal_url_mock(hass): """Mock internal URL of the instance.""" await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index eeda68cd2d3..2ce298640de 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -65,9 +65,11 @@ async def test_import(hass, tuya): """Test import step.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.tuya.async_setup", return_value=True, + "homeassistant.components.tuya.async_setup", + return_value=True, ) as mock_setup, patch( - "homeassistant.components.tuya.async_setup_entry", return_value=True, + "homeassistant.components.tuya.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index f66014d6557..33afde2a076 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -75,7 +75,8 @@ async def test_offline(hass): twitch_mock.streams.get_stream_by_user.return_value = None with patch( - "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, + "homeassistant.components.twitch.sensor.TwitchClient", + return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True await hass.async_block_till_done() @@ -94,7 +95,8 @@ async def test_streaming(hass): twitch_mock.streams.get_stream_by_user.return_value = STREAM_OBJECT_ONLINE with patch( - "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, + "homeassistant.components.twitch.sensor.TwitchClient", + return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True await hass.async_block_till_done() @@ -118,7 +120,8 @@ async def test_oauth_without_sub_and_follow(hass): twitch_mock.users.check_follows_channel.side_effect = HTTPError() with patch( - "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, + "homeassistant.components.twitch.sensor.TwitchClient", + return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) await hass.async_block_till_done() @@ -140,7 +143,8 @@ async def test_oauth_with_sub(hass): twitch_mock.users.check_follows_channel.side_effect = HTTPError() with patch( - "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, + "homeassistant.components.twitch.sensor.TwitchClient", + return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) await hass.async_block_till_done() @@ -164,7 +168,8 @@ async def test_oauth_with_follow(hass): twitch_mock.users.check_follows_channel.return_value = FOLLOW_ACTIVE with patch( - "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, + "homeassistant.components.twitch.sensor.TwitchClient", + return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) await hass.async_block_till_done() diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 55483d135b6..5600f454336 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -130,7 +130,8 @@ async def setup_unifi_integration( return {} with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( - "aiounifi.Controller.login", return_value=True, + "aiounifi.Controller.login", + return_value=True, ), patch("aiounifi.Controller.sites", return_value=sites), patch( "aiounifi.Controller.site_description", return_value=site_description ), patch( diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 8f0df236c75..3fef8a16d68 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -265,7 +265,8 @@ async def test_tracked_clients(hass): async def test_tracked_devices(hass): """Test the update_items function with some devices.""" controller = await setup_unifi_integration( - hass, devices_response=[DEVICE_1, DEVICE_2], + hass, + devices_response=[DEVICE_1, DEVICE_2], ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 @@ -360,7 +361,9 @@ async def test_remove_clients(hass): async def test_controller_state_change(hass): """Verify entities state reflect on controller becoming unavailable.""" controller = await setup_unifi_integration( - hass, clients_response=[CLIENT_1], devices_response=[DEVICE_1], + hass, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 @@ -390,7 +393,9 @@ async def test_controller_state_change(hass): async def test_option_track_clients(hass): """Test the tracking of clients can be turned off.""" controller = await setup_unifi_integration( - hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], + hass, + clients_response=[CLIENT_1, CLIENT_2], + devices_response=[DEVICE_1], ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 @@ -404,7 +409,8 @@ async def test_option_track_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_TRACK_CLIENTS: False}, + controller.config_entry, + options={CONF_TRACK_CLIENTS: False}, ) await hass.async_block_till_done() @@ -418,7 +424,8 @@ async def test_option_track_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_TRACK_CLIENTS: True}, + controller.config_entry, + options={CONF_TRACK_CLIENTS: True}, ) await hass.async_block_till_done() @@ -435,7 +442,9 @@ async def test_option_track_clients(hass): async def test_option_track_wired_clients(hass): """Test the tracking of wired clients can be turned off.""" controller = await setup_unifi_integration( - hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], + hass, + clients_response=[CLIENT_1, CLIENT_2], + devices_response=[DEVICE_1], ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 @@ -449,7 +458,8 @@ async def test_option_track_wired_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_TRACK_WIRED_CLIENTS: False}, + controller.config_entry, + options={CONF_TRACK_WIRED_CLIENTS: False}, ) await hass.async_block_till_done() @@ -463,7 +473,8 @@ async def test_option_track_wired_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_TRACK_WIRED_CLIENTS: True}, + controller.config_entry, + options={CONF_TRACK_WIRED_CLIENTS: True}, ) await hass.async_block_till_done() @@ -480,7 +491,9 @@ async def test_option_track_wired_clients(hass): async def test_option_track_devices(hass): """Test the tracking of devices can be turned off.""" controller = await setup_unifi_integration( - hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], + hass, + clients_response=[CLIENT_1, CLIENT_2], + devices_response=[DEVICE_1], ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 @@ -494,7 +507,8 @@ async def test_option_track_devices(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_TRACK_DEVICES: False}, + controller.config_entry, + options={CONF_TRACK_DEVICES: False}, ) await hass.async_block_till_done() @@ -508,7 +522,8 @@ async def test_option_track_devices(hass): assert device_1 is None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_TRACK_DEVICES: True}, + controller.config_entry, + options={CONF_TRACK_DEVICES: True}, ) await hass.async_block_till_done() @@ -544,7 +559,8 @@ async def test_option_ssid_filter(hass): # Setting SSID filter will remove clients outside of filter hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_SSID_FILTER: ["ssid"]}, + controller.config_entry, + options={CONF_SSID_FILTER: ["ssid"]}, ) await hass.async_block_till_done() @@ -578,7 +594,8 @@ async def test_option_ssid_filter(hass): # Remove SSID filter hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_SSID_FILTER: []}, + controller.config_entry, + options={CONF_SSID_FILTER: []}, ) event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_copy]} controller.api.message_handler(event) @@ -788,7 +805,8 @@ async def test_dont_track_clients(hass): assert device_1 is not None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_TRACK_CLIENTS: True}, + controller.config_entry, + options={CONF_TRACK_CLIENTS: True}, ) await hass.async_block_till_done() @@ -818,7 +836,8 @@ async def test_dont_track_devices(hass): assert device_1 is None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_TRACK_DEVICES: True}, + controller.config_entry, + options={CONF_TRACK_DEVICES: True}, ) await hass.async_block_till_done() @@ -847,7 +866,8 @@ async def test_dont_track_wired_clients(hass): assert client_2 is None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_TRACK_WIRED_CLIENTS: True}, + controller.config_entry, + options={CONF_TRACK_WIRED_CLIENTS: True}, ) await hass.async_block_till_done() diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index a768e61468d..2d5fbe96e0f 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -60,7 +60,8 @@ async def test_platform_manually_configured(hass): async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" controller = await setup_unifi_integration( - hass, options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, + hass, + options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, ) assert len(controller.mock_requests) == 4 @@ -110,7 +111,8 @@ async def test_sensors(hass): assert wireless_client_tx.state == "6789.0" hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_ALLOW_BANDWIDTH_SENSORS: False}, + controller.config_entry, + options={CONF_ALLOW_BANDWIDTH_SENSORS: False}, ) await hass.async_block_till_done() @@ -121,7 +123,8 @@ async def test_sensors(hass): assert wireless_client_tx is None hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, + controller.config_entry, + options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, ) await hass.async_block_till_done() @@ -135,7 +138,9 @@ async def test_sensors(hass): async def test_remove_sensors(hass): """Test the remove_items function with some clients.""" controller = await setup_unifi_integration( - hass, options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, clients_response=CLIENTS, + hass, + options={CONF_ALLOW_BANDWIDTH_SENSORS: True}, + clients_response=CLIENTS, ) assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 4 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index ea198c6d8f4..6c4fc25d828 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -265,7 +265,8 @@ async def test_platform_manually_configured(hass): async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" controller = await setup_unifi_integration( - hass, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, + hass, + options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, ) assert len(controller.mock_requests) == 4 @@ -517,21 +518,24 @@ async def test_option_block_clients(hass): # Remove the second switch again hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, + controller.config_entry, + options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Enable one and remove another one hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, + controller.config_entry, + options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 # Remove one hass.config_entries.async_update_entry( - controller.config_entry, options={CONF_BLOCK_CLIENT: []}, + controller.config_entry, + options={CONF_BLOCK_CLIENT: []}, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 @@ -604,7 +608,9 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass): clients will be transparently marked as having POE as well. """ controller = await setup_unifi_integration( - hass, clients_response=POE_SWITCH_CLIENTS, devices_response=[DEVICE_1], + hass, + clients_response=POE_SWITCH_CLIENTS, + devices_response=[DEVICE_1], ) assert len(controller.mock_requests) == 4 diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 52fbab19539..5333f72b2a9 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -860,11 +860,16 @@ async def test_reload(hass): ) yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "universal/configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "universal/configuration.yaml", ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "universal", SERVICE_RELOAD, {}, blocking=True, + "universal", + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 870aa13fc41..98df710d8fe 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -54,7 +54,8 @@ async def test_flow_ssdp_discovery(hass: HomeAssistantType): # Confirm via step ssdp_confirm. result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={}, + result["flow_id"], + user_input={}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -119,7 +120,8 @@ async def test_flow_user(hass: HomeAssistantType): # Confirmed via step user. result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"usn": usn}, + result["flow_id"], + user_input={"usn": usn}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -198,12 +200,15 @@ async def test_options_flow(hass: HomeAssistantType): assert coordinator.update_interval == timedelta(seconds=DEFAULT_SCAN_INTERVAL) # Options flow with no input results in form. - result = await hass.config_entries.options.async_init(config_entry.entry_id,) + result = await hass.config_entries.options.async_init( + config_entry.entry_id, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM # Options flow with input results in update to entry. result2 = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONFIG_ENTRY_SCAN_INTERVAL: 60}, + result["flow_id"], + user_input={CONFIG_ENTRY_SCAN_INTERVAL: 60}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index f52bf375d8e..f11f3ea5a3b 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -87,7 +87,9 @@ async def test_climate( assert hass.states.get(entity_id).state == HVAC_MODE_OFF await hass.services.async_call( - "climate", "set_fan_mode", {"entity_id": entity_id, "fan_mode": "on"}, + "climate", + "set_fan_mode", + {"entity_id": entity_id, "fan_mode": "on"}, ) await hass.async_block_till_done() vera_device.turn_auto_on.assert_called() @@ -97,7 +99,9 @@ async def test_climate( assert hass.states.get(entity_id).attributes["fan_mode"] == FAN_ON await hass.services.async_call( - "climate", "set_fan_mode", {"entity_id": entity_id, "fan_mode": "off"}, + "climate", + "set_fan_mode", + {"entity_id": entity_id, "fan_mode": "off"}, ) await hass.async_block_till_done() vera_device.turn_auto_on.assert_called() @@ -107,7 +111,9 @@ async def test_climate( assert hass.states.get(entity_id).attributes["fan_mode"] == FAN_AUTO await hass.services.async_call( - "climate", "set_temperature", {"entity_id": entity_id, "temperature": 30}, + "climate", + "set_temperature", + {"entity_id": entity_id, "temperature": 30}, ) await hass.async_block_till_done() vera_device.set_temperature.assert_called_with(30) @@ -145,7 +151,9 @@ async def test_climate_f( update_callback = component_data.controller_data.update_callback await hass.services.async_call( - "climate", "set_temperature", {"entity_id": entity_id, "temperature": 30}, + "climate", + "set_temperature", + {"entity_id": entity_id, "temperature": 30}, ) await hass.async_block_till_done() vera_device.set_temperature.assert_called_with(86) diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index a2dae2bd7f8..311c8013d86 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -30,7 +30,9 @@ async def test_cover( assert hass.states.get(entity_id).attributes["current_position"] == 0 await hass.services.async_call( - "cover", "open_cover", {"entity_id": entity_id}, + "cover", + "open_cover", + {"entity_id": entity_id}, ) await hass.async_block_till_done() vera_device.open.assert_called() @@ -42,7 +44,9 @@ async def test_cover( assert hass.states.get(entity_id).attributes["current_position"] == 100 await hass.services.async_call( - "cover", "set_cover_position", {"entity_id": entity_id, "position": 50}, + "cover", + "set_cover_position", + {"entity_id": entity_id, "position": 50}, ) await hass.async_block_till_done() vera_device.set_level.assert_called_with(50) @@ -54,7 +58,9 @@ async def test_cover( assert hass.states.get(entity_id).attributes["current_position"] == 50 await hass.services.async_call( - "cover", "stop_cover", {"entity_id": entity_id}, + "cover", + "stop_cover", + {"entity_id": entity_id}, ) await hass.async_block_till_done() vera_device.stop.assert_called() @@ -64,7 +70,9 @@ async def test_cover( assert hass.states.get(entity_id).attributes["current_position"] == 50 await hass.services.async_call( - "cover", "close_cover", {"entity_id": entity_id}, + "cover", + "close_cover", + {"entity_id": entity_id}, ) await hass.async_block_till_done() vera_device.close.assert_called() diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index 14194d0af52..99391d8d82a 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -32,7 +32,9 @@ async def test_light( assert hass.states.get(entity_id).state == "off" await hass.services.async_call( - "light", "turn_on", {"entity_id": entity_id}, + "light", + "turn_on", + {"entity_id": entity_id}, ) await hass.async_block_till_done() vera_device.switch_on.assert_called() @@ -42,7 +44,9 @@ async def test_light( assert hass.states.get(entity_id).state == "on" await hass.services.async_call( - "light", "turn_on", {"entity_id": entity_id, ATTR_HS_COLOR: [300, 70]}, + "light", + "turn_on", + {"entity_id": entity_id, ATTR_HS_COLOR: [300, 70]}, ) await hass.async_block_till_done() vera_device.set_color.assert_called_with((255, 76, 255)) @@ -54,7 +58,9 @@ async def test_light( assert hass.states.get(entity_id).attributes["hs_color"] == (300.0, 70.196) await hass.services.async_call( - "light", "turn_on", {"entity_id": entity_id, ATTR_BRIGHTNESS: 55}, + "light", + "turn_on", + {"entity_id": entity_id, ATTR_BRIGHTNESS: 55}, ) await hass.async_block_till_done() vera_device.set_brightness.assert_called_with(55) @@ -66,7 +72,9 @@ async def test_light( assert hass.states.get(entity_id).attributes["brightness"] == 55 await hass.services.async_call( - "light", "turn_off", {"entity_id": entity_id}, + "light", + "turn_off", + {"entity_id": entity_id}, ) await hass.async_block_till_done() vera_device.switch_off.assert_called() diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index 901e09040e9..11af1f5a7b7 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -29,7 +29,9 @@ async def test_lock( assert hass.states.get(entity_id).state == STATE_UNLOCKED await hass.services.async_call( - "lock", "lock", {"entity_id": entity_id}, + "lock", + "lock", + {"entity_id": entity_id}, ) await hass.async_block_till_done() vera_device.lock.assert_called() @@ -39,7 +41,9 @@ async def test_lock( assert hass.states.get(entity_id).state == STATE_LOCKED await hass.services.async_call( - "lock", "unlock", {"entity_id": entity_id}, + "lock", + "unlock", + {"entity_id": entity_id}, ) await hass.async_block_till_done() vera_device.unlock.assert_called() diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index 8f96b7a133a..29ef338b9f1 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -18,10 +18,13 @@ async def test_scene( entity_id = "scene.dev1_1" await vera_component_factory.configure_component( - hass=hass, controller_config=new_simple_controller_config(scenes=(vera_scene,)), + hass=hass, + controller_config=new_simple_controller_config(scenes=(vera_scene,)), ) await hass.services.async_call( - "scene", "turn_on", {"entity_id": entity_id}, + "scene", + "turn_on", + {"entity_id": entity_id}, ) await hass.async_block_till_done() diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index 2a8bfe68185..60e31add4bd 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -28,7 +28,9 @@ async def test_switch( assert hass.states.get(entity_id).state == "off" await hass.services.async_call( - "switch", "turn_on", {"entity_id": entity_id}, + "switch", + "turn_on", + {"entity_id": entity_id}, ) await hass.async_block_till_done() vera_device.switch_on.assert_called() @@ -38,7 +40,9 @@ async def test_switch( assert hass.states.get(entity_id).state == "on" await hass.services.async_call( - "switch", "turn_off", {"entity_id": entity_id}, + "switch", + "turn_off", + {"entity_id": entity_id}, ) await hass.async_block_till_done() vera_device.switch_off.assert_called() diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index d9e8a2ffd24..167fc71a78a 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -110,7 +110,8 @@ async def test_form_already_configured(hass): ) with patch("vilfo.Client.ping", return_value=None), patch( - "vilfo.Client.get_board_information", return_value=None, + "vilfo.Client.get_board_information", + return_value=None, ), patch("vilfo.Client.resolve_mac_address", return_value=None): first_flow_result2 = await hass.config_entries.flow.async_configure( first_flow_result1["flow_id"], @@ -122,7 +123,8 @@ async def test_form_already_configured(hass): ) with patch("vilfo.Client.ping", return_value=None), patch( - "vilfo.Client.get_board_information", return_value=None, + "vilfo.Client.get_board_information", + return_value=None, ), patch("vilfo.Client.resolve_mac_address", return_value=None): second_flow_result2 = await hass.config_entries.flow.async_configure( second_flow_result1["flow_id"], diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 5daedf6fa57..492494fc75e 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -107,7 +107,8 @@ def vizio_invalid_pin_failure_fixture(): "homeassistant.components.vizio.config_flow.VizioAsync.start_pair", return_value=MockStartPairingResponse(CH_TYPE, RESPONSE_TOKEN), ), patch( - "homeassistant.components.vizio.config_flow.VizioAsync.pair", return_value=None, + "homeassistant.components.vizio.config_flow.VizioAsync.pair", + return_value=None, ): yield diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index c0d780ed7ba..6b19089057b 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -182,7 +182,8 @@ async def _test_setup_speaker( } async with _cm_for_test_setup_without_apps( - audio_settings, vizio_power_state, + audio_settings, + vizio_power_state, ): with patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", @@ -208,7 +209,8 @@ async def _cm_for_test_setup_tv_with_apps( ) async with _cm_for_test_setup_without_apps( - {"volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2), "mute": "Off"}, True, + {"volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2), "mute": "Off"}, + True, ): with patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", @@ -266,7 +268,10 @@ async def _test_service( f"homeassistant.components.vizio.media_player.VizioAsync.{vizio_func_name}" ) as service_call: await hass.services.async_call( - domain, ha_service_name, service_data=service_data, blocking=True, + domain, + ha_service_name, + service_data=service_data, + blocking=True, ) assert service_call.called @@ -423,7 +428,8 @@ async def test_options_update( updated_options = {CONF_VOLUME_STEP: VOLUME_STEP} new_options.update(updated_options) hass.config_entries.async_update_entry( - entry=config_entry, options=new_options, + entry=config_entry, + options=new_options, ) assert config_entry.options == updated_options await _test_service( @@ -565,7 +571,9 @@ async def test_setup_with_apps_additional_apps_config( ) -> None: """Test device setup with apps and apps["additional_configs"] in config.""" async with _cm_for_test_setup_tv_with_apps( - hass, MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG, ADDITIONAL_APP_CONFIG["config"], + hass, + MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG, + ADDITIONAL_APP_CONFIG["config"], ): attr = hass.states.get(ENTITY_ID).attributes assert attr["source_list"].count(CURRENT_APP) == 1 @@ -677,7 +685,8 @@ async def test_setup_tv_without_mute( ) async with _cm_for_test_setup_without_apps( - {"volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2)}, STATE_ON, + {"volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2)}, + STATE_ON, ): await _add_config_entry_to_hass(hass, config_entry) diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index b0532ce55d8..a80d527b3f8 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -43,10 +43,12 @@ async def test_form(hass): ), patch( "homeassistant.components.volumio.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.volumio.async_setup_entry", return_value=True, + "homeassistant.components.volumio.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_CONNECTION, + result["flow_id"], + TEST_CONNECTION, ) assert result2["type"] == "create_entry" @@ -80,10 +82,12 @@ async def test_form_updates_unique_id(hass): "homeassistant.components.volumio.config_flow.Volumio.get_system_info", return_value=TEST_SYSTEM_INFO, ), patch("homeassistant.components.volumio.async_setup", return_value=True), patch( - "homeassistant.components.volumio.async_setup_entry", return_value=True, + "homeassistant.components.volumio.async_setup_entry", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_CONNECTION, + result["flow_id"], + TEST_CONNECTION, ) await hass.async_block_till_done() @@ -107,10 +111,12 @@ async def test_empty_system_info(hass): ), patch( "homeassistant.components.volumio.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.volumio.async_setup_entry", return_value=True, + "homeassistant.components.volumio.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_CONNECTION, + result["flow_id"], + TEST_CONNECTION, ) assert result2["type"] == "create_entry" @@ -138,7 +144,8 @@ async def test_form_cannot_connect(hass): side_effect=CannotConnectError, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_CONNECTION, + result["flow_id"], + TEST_CONNECTION, ) assert result2["type"] == "form" @@ -156,7 +163,8 @@ async def test_form_exception(hass): side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_CONNECTION, + result["flow_id"], + TEST_CONNECTION, ) assert result2["type"] == "form" @@ -176,10 +184,12 @@ async def test_discovery(hass): ), patch( "homeassistant.components.volumio.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.volumio.async_setup_entry", return_value=True, + "homeassistant.components.volumio.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={}, + result["flow_id"], + user_input={}, ) assert result2["type"] == "create_entry" @@ -206,7 +216,8 @@ async def test_discovery_cannot_connect(hass): side_effect=CannotConnectError, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={}, + result["flow_id"], + user_input={}, ) assert result2["type"] == "abort" @@ -247,7 +258,8 @@ async def test_discovery_updates_unique_id(hass): with patch( "homeassistant.components.volumio.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.volumio.async_setup_entry", return_value=True, + "homeassistant.components.volumio.async_setup_entry", + return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "zeroconf"}, data=TEST_DISCOVERY diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index 9051b6325bf..44c45c1e9d8 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -37,7 +37,8 @@ async def test_unregistering_webhook(hass, mock_client): async def test_generate_webhook_url(hass): """Test we generate a webhook url correctly.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) url = hass.components.webhook.async_generate_url("some_id") diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 3b213303281..1b85daa1922 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -50,7 +50,9 @@ def client_fixture(): async def setup_webostv(hass): """Initialize webostv and media_player for tests.""" assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake", CONF_NAME: NAME}}, + hass, + DOMAIN, + {DOMAIN: {CONF_HOST: "fake", CONF_NAME: NAME}}, ) await hass.async_block_till_done() diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index 87e119d2c6a..cd129d112bc 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -82,7 +82,8 @@ async def test_form(hass, dummy_tcp_server): assert result["step_id"] == config_entries.SOURCE_USER result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_CONFIG, + result["flow_id"], + user_input=MOCK_CONFIG, ) assert result2["type"] == RESULT_TYPE_CREATE_ENTRY @@ -94,7 +95,8 @@ async def test_form_addr_in_use(hass, addr_in_use): ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_CONFIG, + result["flow_id"], + user_input=MOCK_CONFIG, ) assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "addr_in_use" @@ -107,7 +109,8 @@ async def test_form_start_server_failed(hass, start_server_failed): ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_CONFIG, + result["flow_id"], + user_input=MOCK_CONFIG, ) assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "start_server_failed" diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py index 9c7ba13fc7c..e1c31345235 100644 --- a/tests/components/wilight/__init__.py +++ b/tests/components/wilight/__init__.py @@ -62,7 +62,9 @@ MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN = { } -async def setup_integration(hass: HomeAssistantType,) -> MockConfigEntry: +async def setup_integration( + hass: HomeAssistantType, +) -> MockConfigEntry: """Mock ConfigEntry in Home Assistant.""" entry = MockConfigEntry( diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index 1de190c51d9..7ca6b3241ff 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -35,7 +35,8 @@ def mock_dummy_get_components_from_model(): """Mock a clear components list.""" components = [] with patch( - "pywilight.get_components_from_model", return_value=components, + "pywilight.get_components_from_model", + return_value=components, ): yield components @@ -122,7 +123,9 @@ async def test_ssdp_device_exists_abort(hass: HomeAssistantType) -> None: discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data=discovery_info, ) assert result["type"] == RESULT_TYPE_ABORT diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index 11b86d0c367..01ed57fdcd1 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -36,7 +36,8 @@ def mock_dummy_device_from_host(): device.set_dummy(True) with patch( - "pywilight.device_from_host", return_value=device, + "pywilight.device_from_host", + return_value=device, ): yield device diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py index 555c1487bec..4d4a32604ad 100644 --- a/tests/components/wilight/test_light.py +++ b/tests/components/wilight/test_light.py @@ -36,7 +36,8 @@ def mock_dummy_get_components_from_model_light(): """Mock a components list with light.""" components = ["light"] with patch( - "pywilight.get_components_from_model", return_value=components, + "pywilight.get_components_from_model", + return_value=components, ): yield components @@ -56,7 +57,8 @@ def mock_dummy_device_from_host_light_fan(): device.set_dummy(True) with patch( - "pywilight.device_from_host", return_value=device, + "pywilight.device_from_host", + return_value=device, ): yield device @@ -76,7 +78,8 @@ def mock_dummy_device_from_host_pb(): device.set_dummy(True) with patch( - "pywilight.device_from_host", return_value=device, + "pywilight.device_from_host", + return_value=device, ): yield device @@ -96,7 +99,8 @@ def mock_dummy_device_from_host_dimmer(): device.set_dummy(True) with patch( - "pywilight.device_from_host", return_value=device, + "pywilight.device_from_host", + return_value=device, ): yield device @@ -116,7 +120,8 @@ def mock_dummy_device_from_host_color(): device.set_dummy(True) with patch( - "pywilight.device_from_host", return_value=device, + "pywilight.device_from_host", + return_value=device, ): yield device diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 3ed3b39daee..a09876868a7 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -252,7 +252,8 @@ class ComponentFactory: data_manager = get_data_manager_by_user_id(self._hass, user_id) self._aioclient_mock.clear_requests() self._aioclient_mock.request( - "HEAD", data_manager.webhook_config.url, + "HEAD", + data_manager.webhook_config.url, ) return self._api_class_mock.return_value diff --git a/tests/components/withings/test_binary_sensor.py b/tests/components/withings/test_binary_sensor.py index 8f3347c8867..3477671ea79 100644 --- a/tests/components/withings/test_binary_sensor.py +++ b/tests/components/withings/test_binary_sensor.py @@ -21,7 +21,9 @@ async def test_binary_sensor( person0 = new_profile_config("person0", 0) person1 = new_profile_config("person1", 1) - entity_registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() + entity_registry: EntityRegistry = ( + await hass.helpers.entity_registry.async_get_registry() + ) await component_factory.configure_component(profile_configs=(person0, person1)) assert not await async_get_entity_id(hass, in_bed_attribute, person0.user_id) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 1e9711a71a6..2bf71ab7aa5 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -104,7 +104,9 @@ async def test_webhook_post( async def test_webhook_head( - hass: HomeAssistant, component_factory: ComponentFactory, aiohttp_client, + hass: HomeAssistant, + component_factory: ComponentFactory, + aiohttp_client, ) -> None: """Test head method on webhook view.""" person0 = new_profile_config("person0", 0) @@ -119,7 +121,9 @@ async def test_webhook_head( async def test_webhook_put( - hass: HomeAssistant, component_factory: ComponentFactory, aiohttp_client, + hass: HomeAssistant, + component_factory: ComponentFactory, + aiohttp_client, ) -> None: """Test webhook callback.""" person0 = new_profile_config("person0", 0) @@ -187,7 +191,9 @@ async def test_data_manager_webhook_subscription( aioclient_mock.clear_requests() aioclient_mock.request( - "HEAD", data_manager.webhook_config.url, status=200, + "HEAD", + data_manager.webhook_config.url, + status=200, ) # Test subscribing diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index f47a8f95e53..2b4776c0ca5 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -65,7 +65,10 @@ async def test_config_reauth_profile( assert result["step_id"] == "reauth" assert result["description_placeholders"] == {const.PROFILE: "person0"} - result = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 3370c23e3d8..16b83a585aa 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -304,7 +304,9 @@ async def test_sensor_default_enabled_entities( hass: HomeAssistant, component_factory: ComponentFactory ) -> None: """Test entities enabled by default.""" - entity_registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() + entity_registry: EntityRegistry = ( + await hass.helpers.entity_registry.async_get_registry() + ) await component_factory.configure_component(profile_configs=(PERSON0,)) @@ -345,7 +347,9 @@ async def test_all_entities( hass: HomeAssistant, component_factory: ComponentFactory ) -> None: """Test all entities.""" - entity_registry: EntityRegistry = await hass.helpers.entity_registry.async_get_registry() + entity_registry: EntityRegistry = ( + await hass.helpers.entity_registry.async_get_registry() + ) with patch( "homeassistant.components.withings.sensor.BaseWithingsSensor.entity_registry_enabled_default" diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index f5f1ec3099c..a0c5c11e7ce 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -18,7 +18,8 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + config_flow.DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" @@ -193,7 +194,8 @@ async def test_full_user_flow_implementation( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + config_flow.DOMAIN, + context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 35e8ea0f7ef..66aa50cb71b 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -114,7 +114,9 @@ async def test_segment_change_state( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - on=False, segment_id=0, transition=50, + on=False, + segment_id=0, + transition=50, ) with patch("wled.WLED.segment") as light_mock: @@ -149,7 +151,9 @@ async def test_segment_change_state( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - color_primary=(255, 159, 70), on=True, segment_id=0, + color_primary=(255, 159, 70), + on=True, + segment_id=0, ) @@ -168,7 +172,8 @@ async def test_master_change_state( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - on=False, transition=50, + on=False, + transition=50, ) with patch("wled.WLED.master") as light_mock: @@ -184,7 +189,9 @@ async def test_master_change_state( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - brightness=42, on=True, transition=50, + brightness=42, + on=True, + transition=50, ) with patch("wled.WLED.master") as light_mock: @@ -196,7 +203,8 @@ async def test_master_change_state( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - on=False, transition=50, + on=False, + transition=50, ) with patch("wled.WLED.master") as light_mock: @@ -212,7 +220,9 @@ async def test_master_change_state( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - brightness=42, on=True, transition=50, + brightness=42, + on=True, + transition=50, ) @@ -231,7 +241,8 @@ async def test_dynamically_handle_segments( # Test removal if segment went missing, including the master entity with patch( - "homeassistant.components.wled.WLED.update", return_value=device, + "homeassistant.components.wled.WLED.update", + return_value=device, ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -259,7 +270,8 @@ async def test_single_segment_behavior( # Test absent master with patch( - "homeassistant.components.wled.WLED.update", return_value=device, + "homeassistant.components.wled.WLED.update", + return_value=device, ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -274,7 +286,8 @@ async def test_single_segment_behavior( device.state.brightness = 100 device.state.segments[0].brightness = 255 with patch( - "homeassistant.components.wled.WLED.update", return_value=device, + "homeassistant.components.wled.WLED.update", + return_value=device, ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -286,7 +299,8 @@ async def test_single_segment_behavior( # Test segment is off when master is off device.state.on = False with patch( - "homeassistant.components.wled.WLED.update", return_value=device, + "homeassistant.components.wled.WLED.update", + return_value=device, ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() @@ -304,7 +318,8 @@ async def test_single_segment_behavior( ) await hass.async_block_till_done() master_mock.assert_called_once_with( - on=False, transition=50, + on=False, + transition=50, ) # Test master is turned on when turning on a single segment, and segment @@ -389,7 +404,9 @@ async def test_rgbw_light( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - on=True, segment_id=0, color_primary=(255, 159, 70, 139), + on=True, + segment_id=0, + color_primary=(255, 159, 70, 139), ) with patch("wled.WLED.segment") as light_mock: @@ -401,7 +418,9 @@ async def test_rgbw_light( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - color_primary=(255, 0, 0, 100), on=True, segment_id=0, + color_primary=(255, 0, 0, 100), + on=True, + segment_id=0, ) with patch("wled.WLED.segment") as light_mock: @@ -417,7 +436,9 @@ async def test_rgbw_light( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - color_primary=(0, 0, 0, 100), on=True, segment_id=0, + color_primary=(0, 0, 0, 100), + on=True, + segment_id=0, ) @@ -442,7 +463,11 @@ async def test_effect_service( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - effect="Rainbow", intensity=200, reverse=True, segment_id=0, speed=100, + effect="Rainbow", + intensity=200, + reverse=True, + segment_id=0, + speed=100, ) with patch("wled.WLED.segment") as light_mock: @@ -454,7 +479,8 @@ async def test_effect_service( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - segment_id=0, effect=9, + segment_id=0, + effect=9, ) with patch("wled.WLED.segment") as light_mock: @@ -471,7 +497,10 @@ async def test_effect_service( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - intensity=200, reverse=True, segment_id=0, speed=100, + intensity=200, + reverse=True, + segment_id=0, + speed=100, ) with patch("wled.WLED.segment") as light_mock: @@ -488,7 +517,10 @@ async def test_effect_service( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - effect="Rainbow", reverse=True, segment_id=0, speed=100, + effect="Rainbow", + reverse=True, + segment_id=0, + speed=100, ) with patch("wled.WLED.segment") as light_mock: @@ -505,7 +537,10 @@ async def test_effect_service( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - effect="Rainbow", intensity=200, segment_id=0, speed=100, + effect="Rainbow", + intensity=200, + segment_id=0, + speed=100, ) with patch("wled.WLED.segment") as light_mock: @@ -522,7 +557,10 @@ async def test_effect_service( ) await hass.async_block_till_done() light_mock.assert_called_once_with( - effect="Rainbow", intensity=200, reverse=True, segment_id=0, + effect="Rainbow", + intensity=200, + reverse=True, + segment_id=0, ) diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index f2074f482eb..897650b48e8 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -66,7 +66,8 @@ async def test_create_entry(hass): ) result_create_entry = await hass.config_entries.flow.async_configure( - result["flow_id"], {"device_name": CONFIG[DEVICE_NAME]}, + result["flow_id"], + {"device_name": CONFIG[DEVICE_NAME]}, ) assert result_create_entry["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -132,7 +133,8 @@ async def test_already_configured_error(hass): ) result_create_entry = await hass.config_entries.flow.async_configure( - result["flow_id"], {"device_name": CONFIG[DEVICE_NAME]}, + result["flow_id"], + {"device_name": CONFIG[DEVICE_NAME]}, ) assert result_create_entry["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index 06fda84c934..c84758f8b63 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -88,7 +88,8 @@ async def test_config_flow_user_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, + result["flow_id"], + {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) assert result["type"] == "form" @@ -96,7 +97,8 @@ async def test_config_flow_user_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, + result["flow_id"], + {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) assert result["type"] == "create_entry" @@ -129,7 +131,8 @@ async def test_config_flow_user_multiple_success(hass): return_value=mock_gateway_discovery, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, + result["flow_id"], + {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) assert result["type"] == "form" @@ -137,7 +140,8 @@ async def test_config_flow_user_multiple_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"select_ip": TEST_HOST_2}, + result["flow_id"], + {"select_ip": TEST_HOST_2}, ) assert result["type"] == "form" @@ -145,7 +149,8 @@ async def test_config_flow_user_multiple_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, + result["flow_id"], + {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) assert result["type"] == "create_entry" @@ -172,7 +177,8 @@ async def test_config_flow_user_no_key_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, + result["flow_id"], + {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) assert result["type"] == "form" @@ -180,7 +186,8 @@ async def test_config_flow_user_no_key_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_NAME: TEST_NAME}, + result["flow_id"], + {CONF_NAME: TEST_NAME}, ) assert result["type"] == "create_entry" @@ -226,7 +233,8 @@ async def test_config_flow_user_host_mac_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_NAME: TEST_NAME}, + result["flow_id"], + {CONF_NAME: TEST_NAME}, ) assert result["type"] == "create_entry" @@ -259,7 +267,8 @@ async def test_config_flow_user_discovery_error(hass): return_value=mock_gateway_discovery, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, + result["flow_id"], + {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) assert result["type"] == "form" @@ -284,7 +293,8 @@ async def test_config_flow_user_invalid_interface(hass): return_value=mock_gateway_discovery, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, + result["flow_id"], + {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) assert result["type"] == "form" @@ -369,7 +379,8 @@ async def test_config_flow_user_invalid_key(hass): return_value=mock_gateway_discovery, ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, + result["flow_id"], + {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) assert result["type"] == "form" @@ -377,7 +388,8 @@ async def test_config_flow_user_invalid_key(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, + result["flow_id"], + {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) assert result["type"] == "form" @@ -402,7 +414,8 @@ async def test_zeroconf_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, + result["flow_id"], + {const.CONF_INTERFACE: config_flow.DEFAULT_INTERFACE}, ) assert result["type"] == "form" @@ -410,7 +423,8 @@ async def test_zeroconf_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, + result["flow_id"], + {const.CONF_KEY: TEST_KEY, CONF_NAME: TEST_NAME}, ) assert result["type"] == "create_entry" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 01ae690bc25..d620e739e18 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -50,7 +50,10 @@ async def test_config_flow_step_user_no_device(hass): assert result["step_id"] == "user" assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result["type"] == "form" assert result["step_id"] == "user" @@ -68,7 +71,8 @@ async def test_config_flow_step_gateway_connect_error(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {config_flow.CONF_GATEWAY: True}, + result["flow_id"], + {config_flow.CONF_GATEWAY: True}, ) assert result["type"] == "form" @@ -100,7 +104,8 @@ async def test_config_flow_gateway_success(hass): assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( - result["flow_id"], {config_flow.CONF_GATEWAY: True}, + result["flow_id"], + {config_flow.CONF_GATEWAY: True}, ) assert result["type"] == "form" @@ -162,7 +167,8 @@ async def test_zeroconf_gateway_success(hass): "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN}, + result["flow_id"], + {CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN}, ) assert result["type"] == "create_entry" diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index 0a2095daa6a..e45a93a38b3 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -46,7 +46,11 @@ async def test_multiple_zeroconf_instances_gives_shared(hass, mock_zeroconf, cap lineno="23", line="self.light.is_on", ), - Mock(filename="/home/dev/mdns/lights.py", lineno="2", line="something()",), + Mock( + filename="/home/dev/mdns/lights.py", + lineno="2", + line="something()", + ), ], ): assert zeroconf.Zeroconf() == zeroconf_instance diff --git a/tests/components/zerproc/test_config_flow.py b/tests/components/zerproc/test_config_flow.py index 4e9c380910e..9ffafca76db 100644 --- a/tests/components/zerproc/test_config_flow.py +++ b/tests/components/zerproc/test_config_flow.py @@ -22,9 +22,13 @@ async def test_flow_success(hass): ), patch( "homeassistant.components.zerproc.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.zerproc.async_setup_entry", return_value=True, + "homeassistant.components.zerproc.async_setup_entry", + return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result2["type"] == "create_entry" assert result2["title"] == "Zerproc" @@ -50,9 +54,13 @@ async def test_flow_no_devices_found(hass): ), patch( "homeassistant.components.zerproc.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.zerproc.async_setup_entry", return_value=True, + "homeassistant.components.zerproc.async_setup_entry", + return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" @@ -76,9 +84,13 @@ async def test_flow_exceptions_caught(hass): ), patch( "homeassistant.components.zerproc.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.zerproc.async_setup_entry", return_value=True, + "homeassistant.components.zerproc.async_setup_entry", + return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) assert result2["type"] == "abort" assert result2["reason"] == "no_devices_found" diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 8ca949cd44e..fe69e6536d3 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -466,7 +466,11 @@ async def test_target_temperature( @pytest.mark.parametrize( "preset, unoccupied, target_temp", - ((None, 1800, 17), (PRESET_AWAY, 1800, 18), (PRESET_AWAY, None, None),), + ( + (None, 1800, 17), + (PRESET_AWAY, 1800, 18), + (PRESET_AWAY, None, None), + ), ) async def test_target_temperature_high( hass, device_climate_mock, preset, unoccupied, target_temp @@ -502,7 +506,11 @@ async def test_target_temperature_high( @pytest.mark.parametrize( "preset, unoccupied, target_temp", - ((None, 1600, 21), (PRESET_AWAY, 1600, 16), (PRESET_AWAY, None, None),), + ( + (None, 1600, 21), + (PRESET_AWAY, 1600, 16), + (PRESET_AWAY, None, None), + ), ) async def test_target_temperature_low( hass, device_climate_mock, preset, unoccupied, target_temp diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 9894360da46..709b9a0ff22 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -53,7 +53,8 @@ async def test_user_flow(detect_mock, hass): @patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()])) @patch( - "homeassistant.components.zha.config_flow.detect_radios", return_value=None, + "homeassistant.components.zha.config_flow.detect_radios", + return_value=None, ) async def test_user_flow_not_detected(detect_mock, hass): """Test user flow, radio not detected.""" @@ -76,7 +77,8 @@ async def test_user_flow_not_detected(detect_mock, hass): async def test_user_flow_show_form(hass): """Test user step form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, ) assert result["type"] == RESULT_TYPE_FORM @@ -163,7 +165,8 @@ async def test_user_port_config_fail(probe_mock, hass): ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, + result["flow_id"], + user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "port_config" @@ -184,7 +187,8 @@ async def test_user_port_config(probe_mock, hass): ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, + result["flow_id"], + user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) assert result["type"] == "create_entry" diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index d32eac130b0..c6c0e74050d 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -314,7 +314,10 @@ async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): # test cover stop with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): await hass.services.async_call( - DOMAIN, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True, + DOMAIN, + SERVICE_STOP_COVER, + {"entity_id": entity_id}, + blocking=True, ) assert cluster_level.request.call_count == 1 assert cluster_level.request.call_args[0][0] is False diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index a408f655ea3..83a57f1fabf 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -235,12 +235,32 @@ async def test_ota_sw_version(hass, ota_zha_device): "device, last_seen_delta, is_available", ( ("zigpy_device", 0, True), - ("zigpy_device", zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2, True,), - ("zigpy_device", zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2, True,), - ("zigpy_device", zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 2, False,), + ( + "zigpy_device", + zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2, + True, + ), + ( + "zigpy_device", + zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2, + True, + ), + ( + "zigpy_device", + zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 2, + False, + ), ("zigpy_device_mains", 0, True), - ("zigpy_device_mains", zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2, True,), - ("zigpy_device_mains", zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2, False,), + ( + "zigpy_device_mains", + zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2, + True, + ), + ( + "zigpy_device_mains", + zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2, + False, + ), ( "zigpy_device_mains", zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2, diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 9fd01f1de8d..0cd8c58e49f 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -61,7 +61,10 @@ def channels_mock(zha_device_mock): ) @pytest.mark.parametrize("device", DEVICES) async def test_devices( - device, hass_disable_services, zigpy_device_mock, zha_device_joined_restored, + device, + hass_disable_services, + zigpy_device_mock, + zha_device_joined_restored, ): """Test device discovery.""" entity_registry = await homeassistant.helpers.entity_registry.async_get_registry( diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 25fecd2d82c..89d3d270322 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -268,7 +268,9 @@ async def test_temp_uom( async def test_electrical_measurement_init( - hass, zigpy_device_mock, zha_device_joined, + hass, + zigpy_device_mock, + zha_device_joined, ): """Test proper initialization of the electrical measurement cluster.""" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 12b2c59ca81..4cf639e7faf 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -1762,12 +1762,15 @@ class TestZWaveServices(unittest.TestCase): assert node.refresh_value.called assert len(node.refresh_value.mock_calls) == 2 - assert sorted( - [ - node.refresh_value.mock_calls[0][1][0], - node.refresh_value.mock_calls[1][1][0], - ] - ) == sorted([value.value_id, power_value.value_id]) + assert ( + sorted( + [ + node.refresh_value.mock_calls[0][1][0], + node.refresh_value.mock_calls[1][1][0], + ] + ) + == sorted([value.value_id, power_value.value_id]) + ) def test_refresh_node(self): """Test zwave refresh_node service.""" diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 0ce1b786d54..4b02faec573 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -600,50 +600,53 @@ async def test_extract_entities(): async def test_extract_devices(): """Test extracting devices.""" - assert condition.async_extract_devices( - { - "condition": "and", - "conditions": [ - {"condition": "device", "device_id": "abcd", "domain": "light"}, - {"condition": "device", "device_id": "qwer", "domain": "switch"}, - { - "condition": "state", - "entity_id": "sensor.not_a_device", - "state": "100", - }, - { - "condition": "not", - "conditions": [ - { - "condition": "device", - "device_id": "abcd_not", - "domain": "light", - }, - { - "condition": "device", - "device_id": "qwer_not", - "domain": "switch", - }, - ], - }, - { - "condition": "or", - "conditions": [ - { - "condition": "device", - "device_id": "abcd_or", - "domain": "light", - }, - { - "condition": "device", - "device_id": "qwer_or", - "domain": "switch", - }, - ], - }, - ], - } - ) == {"abcd", "qwer", "abcd_not", "qwer_not", "abcd_or", "qwer_or"} + assert ( + condition.async_extract_devices( + { + "condition": "and", + "conditions": [ + {"condition": "device", "device_id": "abcd", "domain": "light"}, + {"condition": "device", "device_id": "qwer", "domain": "switch"}, + { + "condition": "state", + "entity_id": "sensor.not_a_device", + "state": "100", + }, + { + "condition": "not", + "conditions": [ + { + "condition": "device", + "device_id": "abcd_not", + "domain": "light", + }, + { + "condition": "device", + "device_id": "qwer_not", + "domain": "switch", + }, + ], + }, + { + "condition": "or", + "conditions": [ + { + "condition": "device", + "device_id": "abcd_or", + "domain": "light", + }, + { + "condition": "device", + "device_id": "qwer_or", + "domain": "switch", + }, + ], + }, + ], + } + ) + == {"abcd", "qwer", "abcd_not", "qwer_not", "abcd_or", "qwer_or"} + ) async def test_condition_template_error(hass, caplog): diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 7893650d420..adbcca63990 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -259,7 +259,8 @@ async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): flow.hass = hass await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) result = await flow.async_step_user(user_input={}) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 37b8ea5b3fd..227f3e366f3 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -131,7 +131,8 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): async def test_step_discovery(hass, flow_handler, local_impl): """Check flow triggers from discovery.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( @@ -149,7 +150,8 @@ async def test_step_discovery(hass, flow_handler, local_impl): async def test_abort_discovered_multiple(hass, flow_handler, local_impl): """Test if aborts when discovered multiple times.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) flow_handler.async_register_implementation(hass, local_impl) @@ -175,14 +177,18 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl): """Test if abort discovery when entries exists.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) flow_handler.async_register_implementation(hass, local_impl) config_entry_oauth2_flow.async_register_implementation( hass, TEST_DOMAIN, MockOAuth2Implementation() ) - entry = MockConfigEntry(domain=TEST_DOMAIN, data={},) + entry = MockConfigEntry( + domain=TEST_DOMAIN, + data={}, + ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -198,7 +204,8 @@ async def test_full_flow( ): """Check full flow.""" await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) flow_handler.async_register_implementation(hass, local_impl) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 05361fe2006..0373705186f 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -80,7 +80,9 @@ async def test_setup_recovers_when_setup_raises(hass): assert platform2_setup.called -@patch("homeassistant.helpers.entity_component.EntityComponent.async_setup_platform",) +@patch( + "homeassistant.helpers.entity_component.EntityComponent.async_setup_platform", +) @patch("homeassistant.setup.async_setup_component", return_value=True) async def test_setup_does_discovery(mock_setup_component, mock_setup, hass): """Test setup for discovery.""" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 97d8af7d0ee..8f5f8fc501b 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -526,7 +526,10 @@ async def test_restore_states(hass): registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( - "light", "hue", "1234", suggested_object_id="simple", + "light", + "hue", + "1234", + suggested_object_id="simple", ) # Should not be created registry.async_get_or_create( diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 9f68ecdefb2..6daae51403c 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -57,7 +57,11 @@ async def test_extract_frame_integration_with_excluded_intergration(caplog): lineno="23", line="self.light.is_on", ), - Mock(filename="/home/dev/mdns/lights.py", lineno="2", line="something()",), + Mock( + filename="/home/dev/mdns/lights.py", + lineno="2", + line="something()", + ), ], ): found_frame, integration, path = frame.get_integration_frame( diff --git a/tests/helpers/test_location.py b/tests/helpers/test_location.py index 2f99d42616f..219d015bdf7 100644 --- a/tests/helpers/test_location.py +++ b/tests/helpers/test_location.py @@ -62,7 +62,9 @@ async def test_coordinates_function_as_state(hass): async def test_coordinates_function_device_tracker_in_zone(hass): """Test coordinates function.""" hass.states.async_set( - "zone.home", "zoning", {"latitude": 32.87336, "longitude": -117.22943}, + "zone.home", + "zoning", + {"latitude": 32.87336, "longitude": -117.22943}, ) hass.states.async_set("device_tracker.device", "home") assert ( @@ -87,7 +89,8 @@ async def test_coordinates_function_device_tracker_from_input_select(hass): def test_coordinates_function_returns_none_on_recursion(hass): """Test coordinates function.""" hass.states.async_set( - "test.first", "test.second", + "test.first", + "test.second", ) hass.states.async_set("test.second", "test.first") assert location.find_coordinates(hass, "test.first") is None @@ -96,7 +99,8 @@ def test_coordinates_function_returns_none_on_recursion(hass): async def test_coordinates_function_returns_none_if_invalid_coord(hass): """Test test_coordinates function.""" hass.states.async_set( - "test.object", "abc", + "test.object", + "abc", ) assert location.find_coordinates(hass, "test.object") is None diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 1754511d95c..f51ee2090dc 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -26,7 +26,8 @@ async def test_get_url_internal(hass: HomeAssistant): # Test with internal URL: http://example.local:8123 await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:8123"}, + hass, + {"internal_url": "http://example.local:8123"}, ) assert hass.config.internal_url == "http://example.local:8123" @@ -66,7 +67,8 @@ async def test_get_url_internal(hass: HomeAssistant): # Test with internal URL: https://example.local:8123 await async_process_ha_core_config( - hass, {"internal_url": "https://example.local:8123"}, + hass, + {"internal_url": "https://example.local:8123"}, ) assert hass.config.internal_url == "https://example.local:8123" @@ -79,7 +81,8 @@ async def test_get_url_internal(hass: HomeAssistant): # Test with internal URL: http://example.local:80/ await async_process_ha_core_config( - hass, {"internal_url": "http://example.local:80/"}, + hass, + {"internal_url": "http://example.local:80/"}, ) assert hass.config.internal_url == "http://example.local:80/" @@ -92,7 +95,8 @@ async def test_get_url_internal(hass: HomeAssistant): # Test with internal URL: https://example.local:443 await async_process_ha_core_config( - hass, {"internal_url": "https://example.local:443"}, + hass, + {"internal_url": "https://example.local:443"}, ) assert hass.config.internal_url == "https://example.local:443" @@ -105,7 +109,8 @@ async def test_get_url_internal(hass: HomeAssistant): # Test with internal URL: https://192.168.0.1 await async_process_ha_core_config( - hass, {"internal_url": "https://192.168.0.1"}, + hass, + {"internal_url": "https://192.168.0.1"}, ) assert hass.config.internal_url == "https://192.168.0.1" @@ -118,7 +123,8 @@ async def test_get_url_internal(hass: HomeAssistant): # Test with internal URL: http://192.168.0.1:8123 await async_process_ha_core_config( - hass, {"internal_url": "http://192.168.0.1:8123"}, + hass, + {"internal_url": "http://192.168.0.1:8123"}, ) assert hass.config.internal_url == "http://192.168.0.1:8123" @@ -224,7 +230,8 @@ async def test_get_url_external(hass: HomeAssistant): # Test with external URL: http://example.com:8123 await async_process_ha_core_config( - hass, {"external_url": "http://example.com:8123"}, + hass, + {"external_url": "http://example.com:8123"}, ) assert hass.config.external_url == "http://example.com:8123" @@ -266,7 +273,8 @@ async def test_get_url_external(hass: HomeAssistant): # Test with external URL: http://example.com:80/ await async_process_ha_core_config( - hass, {"external_url": "http://example.com:80/"}, + hass, + {"external_url": "http://example.com:80/"}, ) assert hass.config.external_url == "http://example.com:80/" @@ -281,7 +289,8 @@ async def test_get_url_external(hass: HomeAssistant): # Test with external url: https://example.com:443/ await async_process_ha_core_config( - hass, {"external_url": "https://example.com:443/"}, + hass, + {"external_url": "https://example.com:443/"}, ) assert hass.config.external_url == "https://example.com:443/" assert _get_external_url(hass) == "https://example.com" @@ -293,7 +302,8 @@ async def test_get_url_external(hass: HomeAssistant): # Test with external URL: https://example.com:80 await async_process_ha_core_config( - hass, {"external_url": "https://example.com:80"}, + hass, + {"external_url": "https://example.com:80"}, ) assert hass.config.external_url == "https://example.com:80" assert _get_external_url(hass) == "https://example.com:80" @@ -307,7 +317,8 @@ async def test_get_url_external(hass: HomeAssistant): # Test with external URL: https://192.168.0.1 await async_process_ha_core_config( - hass, {"external_url": "https://192.168.0.1"}, + hass, + {"external_url": "https://192.168.0.1"}, ) assert hass.config.external_url == "https://192.168.0.1" assert _get_external_url(hass) == "https://192.168.0.1" @@ -381,7 +392,8 @@ async def test_get_external_url_cloud_fallback(hass: HomeAssistant): # Test with external URL: http://1.1.1.1:8123 await async_process_ha_core_config( - hass, {"external_url": "http://1.1.1.1:8123"}, + hass, + {"external_url": "http://1.1.1.1:8123"}, ) assert hass.config.external_url == "http://1.1.1.1:8123" @@ -406,7 +418,8 @@ async def test_get_external_url_cloud_fallback(hass: HomeAssistant): # Test with external URL: https://example.com await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) assert hass.config.external_url == "https://example.com" @@ -453,7 +466,8 @@ async def test_get_url(hass: HomeAssistant): # Test only external hass.config.api = None await async_process_ha_core_config( - hass, {"external_url": "https://example.com"}, + hass, + {"external_url": "https://example.com"}, ) assert hass.config.external_url == "https://example.com" assert hass.config.internal_url is None @@ -703,7 +717,8 @@ async def test_get_internal_url_with_base_url_fallback(hass: HomeAssistant): # Add internal URL await async_process_ha_core_config( - hass, {"internal_url": "https://internal.local"}, + hass, + {"internal_url": "https://internal.local"}, ) assert _get_internal_url(hass) == "https://internal.local" assert _get_internal_url(hass, allow_ip=False) == "https://internal.local" @@ -714,7 +729,8 @@ async def test_get_internal_url_with_base_url_fallback(hass: HomeAssistant): # Add internal URL, mixed results await async_process_ha_core_config( - hass, {"internal_url": "http://internal.local:8123"}, + hass, + {"internal_url": "http://internal.local:8123"}, ) assert _get_internal_url(hass) == "http://internal.local:8123" assert _get_internal_url(hass, allow_ip=False) == "http://internal.local:8123" @@ -725,7 +741,8 @@ async def test_get_internal_url_with_base_url_fallback(hass: HomeAssistant): # Add internal URL set to an IP await async_process_ha_core_config( - hass, {"internal_url": "http://10.10.10.10:8123"}, + hass, + {"internal_url": "http://10.10.10.10:8123"}, ) assert _get_internal_url(hass) == "http://10.10.10.10:8123" assert _get_internal_url(hass, allow_ip=False) == "https://example.local" @@ -752,7 +769,8 @@ async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant): # Add external URL await async_process_ha_core_config( - hass, {"external_url": "https://external.example.com"}, + hass, + {"external_url": "https://external.example.com"}, ) assert _get_external_url(hass) == "https://external.example.com" assert _get_external_url(hass, allow_ip=False) == "https://external.example.com" @@ -764,7 +782,8 @@ async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant): # Add external URL, mixed results await async_process_ha_core_config( - hass, {"external_url": "http://external.example.com:8123"}, + hass, + {"external_url": "http://external.example.com:8123"}, ) assert _get_external_url(hass) == "http://external.example.com:8123" assert _get_external_url(hass, allow_ip=False) == "http://external.example.com:8123" @@ -773,7 +792,8 @@ async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant): # Add external URL set to an IP await async_process_ha_core_config( - hass, {"external_url": "http://1.1.1.1:8123"}, + hass, + {"external_url": "http://1.1.1.1:8123"}, ) assert _get_external_url(hass) == "http://1.1.1.1:8123" assert _get_external_url(hass, allow_ip=False) == "https://example.com" diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index c1062a488ee..255d3cde40a 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -53,7 +53,9 @@ async def test_reload_platform(hass): assert platform.domain == DOMAIN yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "helpers/reload_configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "helpers/reload_configuration.yaml", ) with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await async_reload_integration_platforms(hass, PLATFORM, [DOMAIN]) @@ -88,11 +90,16 @@ async def test_setup_reload_service(hass): await async_setup_reload_service(hass, PLATFORM, [DOMAIN]) yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "helpers/reload_configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "helpers/reload_configuration.yaml", ) with patch.object(config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - PLATFORM, SERVICE_RELOAD, {}, blocking=True, + PLATFORM, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index e3736aadceb..1a0cdb79bbc 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -44,7 +44,8 @@ SUPPORT_C = 4 def mock_handle_entity_call(): """Mock service platform call.""" with patch( - "homeassistant.helpers.service._handle_entity_call", return_value=None, + "homeassistant.helpers.service._handle_entity_call", + return_value=None, ) as mock_call: yield mock_call @@ -701,7 +702,9 @@ async def test_domain_control_unauthorized(hass, hass_read_only_user): hass, { "light.kitchen": ent_reg.RegistryEntry( - entity_id="light.kitchen", unique_id="kitchen", platform="test_domain", + entity_id="light.kitchen", + unique_id="kitchen", + platform="test_domain", ) }, ) @@ -738,7 +741,9 @@ async def test_domain_control_admin(hass, hass_admin_user): hass, { "light.kitchen": ent_reg.RegistryEntry( - entity_id="light.kitchen", unique_id="kitchen", platform="test_domain", + entity_id="light.kitchen", + unique_id="kitchen", + platform="test_domain", ) }, ) @@ -774,7 +779,9 @@ async def test_domain_control_no_user(hass): hass, { "light.kitchen": ent_reg.RegistryEntry( - entity_id="light.kitchen", unique_id="kitchen", platform="test_domain", + entity_id="light.kitchen", + unique_id="kitchen", + platform="test_domain", ) }, ) @@ -835,7 +842,11 @@ async def test_extract_from_service_available_device(hass): await service.async_extract_entities( hass, entities, - ha.ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_NONE},), + ha.ServiceCall( + "test", + "service", + data={"entity_id": ENTITY_MATCH_NONE}, + ), ) == [] ) diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index b19669d13f4..626f8a83744 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -68,7 +68,9 @@ async def test_call_to_component(hass): context = "dummy_context" await state.async_reproduce_state( - hass, [state_media_player, state_climate], context=context, + hass, + [state_media_player, state_climate], + context=context, ) media_player_fun.assert_called_once_with( diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 9d8b763c275..3b293c106f9 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -872,7 +872,10 @@ def test_relative_time(mock_is_safe, hass): ) assert ( "string" - == template.Template('{{relative_time("string")}}', hass,).async_render() + == template.Template( + '{{relative_time("string")}}', + hass, + ).async_render() ) @@ -2076,13 +2079,16 @@ def test_extract_entities_domain_states_inner(hass, allow_extract_entities): hass.states.async_set("light.switch2", "on") hass.states.async_set("light.switch3", "off") - assert set( - template.extract_entities( - hass, - "{{ states['light'] | selectattr('state','eq','on') | list | count > 0 }}", - {}, + assert ( + set( + template.extract_entities( + hass, + "{{ states['light'] | selectattr('state','eq','on') | list | count > 0 }}", + {}, + ) ) - ) == {"light.switch", "light.switch2", "light.switch3"} + == {"light.switch", "light.switch2", "light.switch3"} + ) def test_extract_entities_domain_states_outer(hass, allow_extract_entities): @@ -2091,13 +2097,16 @@ def test_extract_entities_domain_states_outer(hass, allow_extract_entities): hass.states.async_set("light.switch2", "on") hass.states.async_set("light.switch3", "off") - assert set( - template.extract_entities( - hass, - "{{ states.light | selectattr('state','eq','off') | list | count > 0 }}", - {}, + assert ( + set( + template.extract_entities( + hass, + "{{ states.light | selectattr('state','eq','off') | list | count > 0 }}", + {}, + ) ) - ) == {"light.switch", "light.switch2", "light.switch3"} + == {"light.switch", "light.switch2", "light.switch3"} + ) def test_extract_entities_domain_states_outer_with_group(hass, allow_extract_entities): @@ -2108,20 +2117,25 @@ def test_extract_entities_domain_states_outer_with_group(hass, allow_extract_ent hass.states.async_set("switch.pool_light", "off") hass.states.async_set("group.lights", "off", {"entity_id": ["switch.pool_light"]}) - assert set( - template.extract_entities( - hass, - "{{ states.light | selectattr('entity_id', 'in', state_attr('group.lights', 'entity_id')) }}", - {}, + assert ( + set( + template.extract_entities( + hass, + "{{ states.light | selectattr('entity_id', 'in', state_attr('group.lights', 'entity_id')) }}", + {}, + ) ) - ) == {"light.switch", "light.switch2", "light.switch3", "group.lights"} + == {"light.switch", "light.switch2", "light.switch3", "group.lights"} + ) def test_extract_entities_blocked_from_core_code(hass): """Test extract entities is blocked from core code.""" with pytest.raises(RuntimeError): template.extract_entities( - hass, "{{ states.light }}", {}, + hass, + "{{ states.light }}", + {}, ) @@ -2142,11 +2156,17 @@ def test_extract_entities_warns_and_logs_from_an_integration(hass, caplog): line="do_something()", ), correct_frame, - Mock(filename="/home/dev/mdns/lights.py", lineno="2", line="something()",), + Mock( + filename="/home/dev/mdns/lights.py", + lineno="2", + line="something()", + ), ], ): template.extract_entities( - hass, "{{ states.light }}", {}, + hass, + "{{ states.light }}", + {}, ) assert "custom_components/burncpu/light.py" in caplog.text @@ -2219,7 +2239,8 @@ def test_render_complex_handling_non_template_values(hass): def test_urlencode(hass): """Test the urlencode method.""" tpl = template.Template( - ("{% set dict = {'foo': 'x&y', 'bar': 42} %}" "{{ dict | urlencode }}"), hass, + ("{% set dict = {'foo': 'x&y', 'bar': 42} %}" "{{ dict | urlencode }}"), + hass, ) assert tpl.async_render() == "foo=x%26y&bar=42" tpl = template.Template( @@ -2234,13 +2255,17 @@ async def test_cache_garbage_collection(): template_string = ( "{% set dict = {'foo': 'x&y', 'bar': 42} %} {{ dict | urlencode }}" ) - tpl = template.Template((template_string),) + tpl = template.Template( + (template_string), + ) tpl.ensure_valid() assert template._NO_HASS_ENV.template_cache.get( template_string ) # pylint: disable=protected-access - tpl2 = template.Template((template_string),) + tpl2 = template.Template( + (template_string), + ) tpl2.ensure_valid() assert template._NO_HASS_ENV.template_cache.get( template_string diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 20d32202a1a..e68131d689b 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -1,6 +1,9 @@ """List of tests that have uncaught exceptions today. Will be shrunk over time.""" IGNORE_UNCAUGHT_EXCEPTIONS = [ - ("test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup",), + ( + "test_homeassistant_bridge", + "test_homeassistant_bridge_fan_setup", + ), ( "tests.components.owntracks.test_device_tracker", "test_mobile_multiple_async_enter_exit", diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 03c5ba68e59..59e77dc1388 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -172,7 +172,8 @@ async def test_setup_after_deps_in_stage_1_ignored(hass): mock_integration( hass, MockModule( - domain="an_after_dep", async_setup=gen_domain_setup("an_after_dep"), + domain="an_after_dep", + async_setup=gen_domain_setup("an_after_dep"), ), ) mock_integration( diff --git a/tests/test_config.py b/tests/test_config.py index 22b5987f69c..9ec0c166850 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -958,20 +958,23 @@ async def test_component_config_exceptions(hass, caplog): # component.PLATFORM_SCHEMA caplog.clear() - assert await config_util.async_process_component_config( - hass, - {"test_domain": {"platform": "test_platform"}}, - integration=Mock( - domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock( - return_value=Mock( - spec=["PLATFORM_SCHEMA_BASE"], - PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")), - ) + assert ( + await config_util.async_process_component_config( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock( + return_value=Mock( + spec=["PLATFORM_SCHEMA_BASE"], + PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")), + ) + ), ), - ), - ) == {"test_domain": []} + ) + == {"test_domain": []} + ) assert "ValueError: broken" in caplog.text assert ( "Unknown error validating test_platform platform config with test_domain component platform schema" @@ -990,15 +993,20 @@ async def test_component_config_exceptions(hass, caplog): ) ), ): - assert await config_util.async_process_component_config( - hass, - {"test_domain": {"platform": "test_platform"}}, - integration=Mock( - domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), - ), - ) == {"test_domain": []} + assert ( + await config_util.async_process_component_config( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock( + return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"]) + ), + ), + ) + == {"test_domain": []} + ) assert "ValueError: broken" in caplog.text assert ( "Unknown error validating config for test_platform platform for test_domain component with PLATFORM_SCHEMA" @@ -1025,7 +1033,11 @@ async def test_component_config_exceptions(hass, caplog): ), ("zone", vol.Schema({vol.Optional("zone"): int}), None), ("zone", vol.Schema({"zone": int}), None), - ("not_existing", vol.Schema({vol.Optional("zone", default=dict): dict}), None,), + ( + "not_existing", + vol.Schema({vol.Optional("zone", default=dict): dict}), + None, + ), ("non_existing", vol.Schema({"zone": int}), None), ("zone", vol.Schema({}), None), ("plex", vol.Schema(vol.All({"plex": {"host": str}})), "dict"), diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 816530befea..f13fac02850 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1003,7 +1003,8 @@ async def test_init_custom_integration(hass): ) with pytest.raises(data_entry_flow.UnknownHandler): with patch( - "homeassistant.loader.async_get_integration", return_value=integration, + "homeassistant.loader.async_get_integration", + return_value=integration, ): await hass.config_entries.flow.async_init("bla") @@ -1178,7 +1179,8 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager): entry.add_to_hass(hass) mock_integration( - hass, MockModule("comp"), + hass, + MockModule("comp"), ) mock_entity_platform(hass, "config_flow.comp", None) @@ -1221,7 +1223,8 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): entry.add_to_hass(hass) mock_integration( - hass, MockModule("comp"), + hass, + MockModule("comp"), ) mock_entity_platform(hass, "config_flow.comp", None) updates = {"host": "1.1.1.1"} @@ -1281,7 +1284,8 @@ async def test_unique_id_not_update_existing_entry(hass, manager): entry.add_to_hass(hass) mock_integration( - hass, MockModule("comp"), + hass, + MockModule("comp"), ) mock_entity_platform(hass, "config_flow.comp", None) @@ -1346,7 +1350,8 @@ async def test_unique_id_in_progress(hass, manager): async def test_finish_flow_aborts_progress(hass, manager): """Test that when finishing a flow, we abort other flows in progress with unique ID.""" mock_integration( - hass, MockModule("comp", async_setup_entry=AsyncMock(return_value=True)), + hass, + MockModule("comp", async_setup_entry=AsyncMock(return_value=True)), ) mock_entity_platform(hass, "config_flow.comp", None) @@ -1611,7 +1616,9 @@ async def test_async_setup_init_entry(hass): """Mock setup.""" hass.async_create_task( hass.config_entries.flow.async_init( - "comp", context={"source": config_entries.SOURCE_IMPORT}, data={}, + "comp", + context={"source": config_entries.SOURCE_IMPORT}, + data={}, ) ) return True @@ -1656,7 +1663,9 @@ async def test_async_setup_update_entry(hass): """Mock setup.""" hass.async_create_task( hass.config_entries.flow.async_init( - "comp", context={"source": config_entries.SOURCE_IMPORT}, data={}, + "comp", + context={"source": config_entries.SOURCE_IMPORT}, + data={}, ) ) return True @@ -1714,7 +1723,8 @@ async def test_async_setup_update_entry(hass): async def test_flow_with_default_discovery(hass, manager, discovery_source): """Test that finishing a default discovery flow removes the unique ID in the entry.""" mock_integration( - hass, MockModule("comp", async_setup_entry=AsyncMock(return_value=True)), + hass, + MockModule("comp", async_setup_entry=AsyncMock(return_value=True)), ) mock_entity_platform(hass, "config_flow.comp", None) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index fcc2d571331..6297da0c2d5 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -216,7 +216,8 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest): zeroconf = await loader.async_get_integration(hass, "zeroconf") mock_integration( - hass, MockModule("comp", partial_manifest=partial_manifest), + hass, + MockModule("comp", partial_manifest=partial_manifest), ) with patch( diff --git a/tests/test_setup.py b/tests/test_setup.py index 8651308572a..3d34d1e1383 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -577,7 +577,11 @@ async def test_parallel_entry_setup(hass): return True mock_integration( - hass, MockModule("comp", async_setup_entry=mock_async_setup_entry,), + hass, + MockModule( + "comp", + async_setup_entry=mock_async_setup_entry, + ), ) mock_entity_platform(hass, "config_flow.comp", None) await setup.async_setup_component(hass, "comp", {}) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index c2b6a428515..af858967150 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -149,12 +149,18 @@ def test_find_unserializable_data(): bad_data = object() - assert find_paths_unserializable_data( - [State("mock_domain.mock_entity", "on", {"bad": bad_data})], - dump=partial(dumps, cls=MockJSONEncoder), - ) == {"$[0](state: mock_domain.mock_entity).attributes.bad": bad_data} + assert ( + find_paths_unserializable_data( + [State("mock_domain.mock_entity", "on", {"bad": bad_data})], + dump=partial(dumps, cls=MockJSONEncoder), + ) + == {"$[0](state: mock_domain.mock_entity).attributes.bad": bad_data} + ) - assert find_paths_unserializable_data( - [Event("bad_event", {"bad_attribute": bad_data})], - dump=partial(dumps, cls=MockJSONEncoder), - ) == {"$[0](event: bad_event).data.bad_attribute": bad_data} + assert ( + find_paths_unserializable_data( + [Event("bad_event", {"bad_attribute": bad_data})], + dump=partial(dumps, cls=MockJSONEncoder), + ) + == {"$[0](event: bad_event).data.bad_attribute": bad_data} + ) From 55283df70522d670926b53e47febf870f6685c88 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 27 Aug 2020 14:54:43 +0200 Subject: [PATCH 372/862] Remove protobuf requirement from tensorflow manifest (#39316) --- homeassistant/components/tensorflow/manifest.json | 1 - requirements_all.txt | 3 --- 2 files changed, 4 deletions(-) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index ddb0ec542ba..6b5218b64b1 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -8,7 +8,6 @@ "tf-models-official==2.2.1", "pycocotools==2.0.1", "numpy==1.19.1", - "protobuf==3.12.2", "pillow==7.2.0" ], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index f7386e3e41b..dce9b946b86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1119,9 +1119,6 @@ proliphix==0.4.1 # homeassistant.components.prometheus prometheus_client==0.7.1 -# homeassistant.components.tensorflow -protobuf==3.12.2 - # homeassistant.components.proxmoxve proxmoxer==1.1.1 From b880c33043ac22675d7467c026ce69940e25c70c Mon Sep 17 00:00:00 2001 From: matgad Date: Thu, 27 Aug 2020 15:36:35 +0200 Subject: [PATCH 373/862] Bump zigpy-cc version (#39318) --- 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 b9d2caf0137..bb5da313a56 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.18.1", "pyserial==3.4", "zha-quirks==0.0.43", - "zigpy-cc==0.4.4", + "zigpy-cc==0.5.1", "zigpy-deconz==0.9.2", "zigpy==0.22.2", "zigpy-xbee==0.12.1", diff --git a/requirements_all.txt b/requirements_all.txt index dce9b946b86..8770a45a6f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2289,7 +2289,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-cc==0.4.4 +zigpy-cc==0.5.1 # homeassistant.components.zha zigpy-deconz==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e05ae58e71..ce7fd4090ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1044,7 +1044,7 @@ zeroconf==0.28.1 zha-quirks==0.0.43 # homeassistant.components.zha -zigpy-cc==0.4.4 +zigpy-cc==0.5.1 # homeassistant.components.zha zigpy-deconz==0.9.2 From 98993d8503eed0fbf5454bcada5c23ab038e8bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 27 Aug 2020 17:56:53 +0300 Subject: [PATCH 374/862] Trivial requirements cleanups (#39222) --- requirements_all.txt | 1 - requirements_test_all.txt | 1 - script/gen_requirements_all.py | 12 +++++------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 8770a45a6f1..d6cd0e25a35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,4 @@ # Home Assistant Core, full dependency set --c homeassistant/package_constraints.txt -r requirements.txt # homeassistant.components.nuimo_controller diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce7fd4090ff..d4025ebef3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1,7 +1,6 @@ # Home Assistant tests, full dependency set # Automatically generated by gen_requirements_all.py, do not edit --c homeassistant/package_constraints.txt -r requirements_test.txt # homeassistant.components.homekit diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 07883dc6221..e86953a6280 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -251,8 +251,7 @@ def requirements_output(reqs): def requirements_all_output(reqs): """Generate output for requirements_all.""" output = [ - "# Home Assistant Core, full dependency set\n" - "-c homeassistant/package_constraints.txt\n", + "# Home Assistant Core, full dependency set\n", "-r requirements.txt\n", ] output.append(generate_requirements_list(reqs)) @@ -260,15 +259,14 @@ def requirements_all_output(reqs): return "".join(output) -def requirements_test_output(reqs): +def requirements_test_all_output(reqs): """Generate output for test_requirements.""" output = [ "# Home Assistant tests, full dependency set\n", f"# Automatically generated by {Path(__file__).name}, do not edit\n", "\n", - "-c homeassistant/package_constraints.txt\n", + "-r requirements_test.txt\n", ] - output.append("-r requirements_test.txt\n") filtered = { requirement: modules @@ -347,7 +345,7 @@ def main(validate): reqs_file = requirements_output(data) reqs_all_file = requirements_all_output(data) - reqs_test_file = requirements_test_output(data) + reqs_test_all_file = requirements_test_all_output(data) reqs_pre_commit_file = requirements_pre_commit_output() constraints = gather_constraints() @@ -355,7 +353,7 @@ def main(validate): ("requirements.txt", reqs_file), ("requirements_all.txt", reqs_all_file), ("requirements_test_pre_commit.txt", reqs_pre_commit_file), - ("requirements_test_all.txt", reqs_test_file), + ("requirements_test_all.txt", reqs_test_all_file), ("homeassistant/package_constraints.txt", constraints), ) From 27f3c0a3025e9d5a9b0182cba3094b91a4708418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 27 Aug 2020 17:57:58 +0300 Subject: [PATCH 375/862] Skip install on tox pylint (#39260) We're not operating on the installed package anyway, and necessary dependencies are handled with tox deps. As a nice bonus side effect, doing this sidesteps breakage caused by pip's (up to 20.2.2 at least) behavior of prepending site-packages to sys.path in certain cases, which in turn results in failures e.g. if a version of typing that is incompatible with the (now overridden) stdlib is installed there. And that combined with also pip's behavior of installing a default build system consisting of setuptools and wheel under the hood when it sees our pyproject.toml without a build-system defined would provoke the breakage before we have a chance to uninstall typing. (There are ways around this too, but skipping the install makes the issue moot at least with our current dependency set.) --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 45759d624bf..7829a7a98d7 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ deps = -r{toxinidir}/requirements_test_all.txt [testenv:pylint] +skip_install = True ignore_errors = True deps = -r{toxinidir}/requirements_all.txt From c8d49a8adfd6a4587b79f2361ec7d59ffdc575e4 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 27 Aug 2020 17:00:36 +0200 Subject: [PATCH 376/862] Add Spotify media browser capability (#39240) Co-authored-by: Tobias Sauerwein Co-authored-by: Bram Kragten Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .../components/media_player/const.py | 5 + homeassistant/components/spotify/__init__.py | 20 +- .../components/spotify/config_flow.py | 62 ++++-- homeassistant/components/spotify/const.py | 17 ++ .../components/spotify/media_player.py | 180 +++++++++++++++++- homeassistant/components/spotify/strings.json | 9 +- tests/components/spotify/test_config_flow.py | 118 +++++++++++- 7 files changed, 390 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 0e7e038cdc1..651dcd94eff 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -40,6 +40,11 @@ MEDIA_TYPE_IMAGE = "image" MEDIA_TYPE_URL = "url" MEDIA_TYPE_GAME = "game" MEDIA_TYPE_APP = "app" +MEDIA_TYPE_ALBUM = "album" +MEDIA_TYPE_TRACK = "track" +MEDIA_TYPE_ARTIST = "artist" +MEDIA_TYPE_PODCAST = "podcast" +MEDIA_TYPE_SEASON = "season" SERVICE_CLEAR_PLAYLIST = "clear_playlist" SERVICE_PLAY_MEDIA = "play_media" diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 619bcdb471f..d28875032da 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -1,4 +1,5 @@ """The spotify integration.""" +import logging from spotipy import Spotify, SpotifyException import voluptuous as vol @@ -16,7 +17,15 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from homeassistant.helpers.typing import ConfigType -from .const import DATA_SPOTIFY_CLIENT, DATA_SPOTIFY_ME, DATA_SPOTIFY_SESSION, DOMAIN +from .const import ( + DATA_SPOTIFY_CLIENT, + DATA_SPOTIFY_ME, + DATA_SPOTIFY_SESSION, + DOMAIN, + SPOTIFY_SCOPES, +) + +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { @@ -71,6 +80,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_SPOTIFY_SESSION: session, } + if set(session.token["scope"].split(" ")) <= set(SPOTIFY_SCOPES): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data=entry.data, + ) + ) + hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, MEDIA_PLAYER_DOMAIN) ) diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index d619d3b2b10..14e45d58b39 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -1,12 +1,15 @@ """Config flow for Spotify.""" import logging +from typing import Any, Dict, Optional from spotipy import Spotify +import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import persistent_notification from homeassistant.helpers import config_entry_oauth2_flow -from .const import DOMAIN +from .const import DOMAIN, SPOTIFY_SCOPES _LOGGER = logging.getLogger(__name__) @@ -17,27 +20,25 @@ class SpotifyFlowHandler( """Config flow to handle Spotify OAuth2 authentication.""" DOMAIN = DOMAIN + VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + def __init__(self) -> None: + """Instantiate config flow.""" + super().__init__() + self.entry: Optional[Dict[str, Any]] = None + @property def logger(self) -> logging.Logger: """Return logger.""" return logging.getLogger(__name__) @property - def extra_authorize_data(self) -> dict: + def extra_authorize_data(self) -> Dict[str, Any]: """Extra data that needs to be appended to the authorize url.""" - scopes = [ - # Needed to be able to control playback - "user-modify-playback-state", - # Needed in order to read available devices - "user-read-playback-state", - # Needed to determine if the user has Spotify Premium - "user-read-private", - ] - return {"scope": ",".join(scopes)} + return {"scope": ",".join(SPOTIFY_SCOPES)} - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: Dict[str, Any]) -> Dict[str, Any]: """Create an entry for Spotify.""" spotify = Spotify(auth=data["token"]["access_token"]) @@ -48,6 +49,9 @@ class SpotifyFlowHandler( name = data["id"] = current_user["id"] + if self.entry and self.entry["id"] != current_user["id"]: + return self.async_abort(reason="reauth_account_mismatch") + if current_user.get("display_name"): name = current_user["display_name"] data["name"] = name @@ -55,3 +59,37 @@ class SpotifyFlowHandler( await self.async_set_unique_id(current_user["id"]) return self.async_create_entry(title=name, data=data) + + async def async_step_reauth(self, entry: Dict[str, Any]) -> Dict[str, Any]: + """Perform reauth upon migration of old entries.""" + if entry: + self.entry = entry + + assert self.hass + persistent_notification.async_create( + self.hass, + f"Spotify integration for account {entry['id']} needs to be re-authenticated. Please go to the integrations page to re-configure it.", + "Spotify re-authentication", + "spotify_reauth", + ) + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Confirm reauth dialog.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={"account": self.entry["id"]}, + data_schema=vol.Schema({}), + errors={}, + ) + + assert self.hass + persistent_notification.async_dismiss(self.hass, "spotify_reauth") + + return await self.async_step_pick_implementation( + user_input={"implementation": self.entry["auth_implementation"]} + ) diff --git a/homeassistant/components/spotify/const.py b/homeassistant/components/spotify/const.py index f508c9b2938..6b677aca996 100644 --- a/homeassistant/components/spotify/const.py +++ b/homeassistant/components/spotify/const.py @@ -5,3 +5,20 @@ DOMAIN = "spotify" DATA_SPOTIFY_CLIENT = "spotify_client" DATA_SPOTIFY_ME = "spotify_me" DATA_SPOTIFY_SESSION = "spotify_session" + +SPOTIFY_SCOPES = [ + # Needed to be able to control playback + "user-modify-playback-state", + # Needed in order to read available devices + "user-read-playback-state", + # Needed to determine if the user has Spotify Premium + "user-read-private", + # Needed for media browsing + "playlist-read-private", + "playlist-read-collaborative", + "user-library-read", + "user-top-read", + "user-read-playback-position", + "user-read-recently-played", + "user-follow-read", +] diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 0446500dba2..1b3140327ed 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -11,8 +11,12 @@ from yarl import URL from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TRACK, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -23,6 +27,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET, ) +from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ID, @@ -36,7 +41,13 @@ from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utc_from_timestamp -from .const import DATA_SPOTIFY_CLIENT, DATA_SPOTIFY_ME, DATA_SPOTIFY_SESSION, DOMAIN +from .const import ( + DATA_SPOTIFY_CLIENT, + DATA_SPOTIFY_ME, + DATA_SPOTIFY_SESSION, + DOMAIN, + SPOTIFY_SCOPES, +) _LOGGER = logging.getLogger(__name__) @@ -45,7 +56,8 @@ ICON = "mdi:spotify" SCAN_INTERVAL = timedelta(seconds=30) SUPPORT_SPOTIFY = ( - SUPPORT_NEXT_TRACK + SUPPORT_BROWSE_MEDIA + | SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA @@ -56,6 +68,23 @@ SUPPORT_SPOTIFY = ( | SUPPORT_VOLUME_SET ) +BROWSE_LIMIT = 48 + +PLAYABLE_MEDIA_TYPES = [ + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_TRACK, +] + +LIBRARY_MAP = { + "user_playlists": "Playlists", + "featured_playlists": "Featured Playlists", + "new_releases": "New Releases", + "current_user_top_artists": "Top Artists", + "current_user_recently_played": "Recently played", +} + async def async_setup_entry( hass: HomeAssistant, @@ -108,6 +137,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self._name = f"Spotify {name}" self._session = session self._spotify = spotify + self._scope_ok = set(session.token["scope"].split(" ")) == set(SPOTIFY_SCOPES) self._currently_playing: Optional[dict] = {} self._devices: Optional[List[dict]] = [] @@ -308,9 +338,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity): # Yet, they do generate those types of URI in their official clients. media_id = str(URL(media_id).with_query(None).with_fragment(None)) - if media_type == MEDIA_TYPE_MUSIC: + if media_type in (MEDIA_TYPE_TRACK, MEDIA_TYPE_MUSIC): kwargs["uris"] = [media_id] - elif media_type == MEDIA_TYPE_PLAYLIST: + elif media_type in PLAYABLE_MEDIA_TYPES: kwargs["context_uri"] = media_id else: _LOGGER.error("Media type %s is not supported", media_type) @@ -355,3 +385,145 @@ class SpotifyMediaPlayer(MediaPlayerEntity): devices = self._spotify.devices() or {} self._devices = devices.get("devices", []) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + if not self._scope_ok: + raise NotImplementedError + + if media_content_type in [None, "library"]: + return await self.hass.async_add_executor_job(library_payload) + + payload = { + "media_content_type": media_content_type, + "media_content_id": media_content_id, + } + response = await self.hass.async_add_executor_job( + build_item_response, self._spotify, payload + ) + if response is None: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) + return response + + +def build_item_response(spotify, payload): + """Create response payload for the provided media query.""" + media_content_type = payload.get("media_content_type") + title = None + if media_content_type == "user_playlists": + media = spotify.current_user_playlists(limit=BROWSE_LIMIT) + items = media.get("items", []) + elif media_content_type == "current_user_recently_played": + media = spotify.current_user_recently_played(limit=BROWSE_LIMIT) + items = media.get("items", []) + elif media_content_type == "featured_playlists": + media = spotify.featured_playlists(limit=BROWSE_LIMIT) + items = media.get("playlists", {}).get("items", []) + elif media_content_type == "current_user_top_artists": + media = spotify.current_user_top_artists(limit=BROWSE_LIMIT) + items = media.get("items", []) + elif media_content_type == "new_releases": + media = spotify.new_releases(limit=BROWSE_LIMIT) + items = media.get("albums", {}).get("items", []) + elif media_content_type == MEDIA_TYPE_PLAYLIST: + media = spotify.playlist(payload["media_content_id"]) + items = media.get("tracks", {}).get("items", []) + elif media_content_type == MEDIA_TYPE_ALBUM: + media = spotify.album(payload["media_content_id"]) + items = media.get("tracks", {}).get("items", []) + elif media_content_type == MEDIA_TYPE_ARTIST: + media = spotify.artist_albums(payload["media_content_id"], limit=BROWSE_LIMIT) + title = spotify.artist(payload["media_content_id"]).get("name") + items = media.get("items", []) + else: + media = None + + if media is None: + return None + + response = { + "media_content_id": payload.get("media_content_id"), + "media_content_type": payload.get("media_content_type"), + "can_play": payload.get("media_content_type") in PLAYABLE_MEDIA_TYPES, + "children": [item_payload(item) for item in items], + } + + if "name" in media: + response["title"] = media.get("name") + elif title: + response["title"] = title + else: + response["title"] = LIBRARY_MAP.get(payload["media_content_id"]) + + if "images" in media: + response["thumbnail"] = fetch_image_url(media) + + return response + + +def item_payload(item): + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + if ( + MEDIA_TYPE_TRACK in item + or item.get("type") != MEDIA_TYPE_ALBUM + and "playlists" in item + ): + track = item.get(MEDIA_TYPE_TRACK) + payload = { + "title": track.get("name"), + "thumbnail": fetch_image_url(track.get(MEDIA_TYPE_ALBUM, {})), + "media_content_id": track.get("uri"), + "media_content_type": MEDIA_TYPE_TRACK, + "can_play": True, + } + else: + payload = { + "title": item.get("name"), + "thumbnail": fetch_image_url(item), + "media_content_id": item.get("uri"), + "media_content_type": item.get("type"), + "can_play": item.get("type") in PLAYABLE_MEDIA_TYPES, + } + + if item.get("type") not in [None, MEDIA_TYPE_TRACK]: + payload["can_expand"] = True + + return payload + + +def library_payload(): + """ + Create response payload to describe contents of a specific library. + + Used by async_browse_media. + """ + library_info = { + "title": "Media Library", + "media_content_id": "library", + "media_content_type": "library", + "can_play": False, + "can_expand": True, + "children": [], + } + + for item in [{"name": n, "type": t} for t, n in LIBRARY_MAP.items()]: + library_info["children"].append( + item_payload( + {"name": item["name"], "type": item["type"], "uri": item["type"]} + ) + ) + return library_info + + +def fetch_image_url(item): + """Fetch image url.""" + try: + return item.get("images", [])[0].get("url") + except IndexError: + return diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index c7831e31ca4..85ff9ff267b 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -1,12 +1,17 @@ { "config": { "step": { - "pick_implementation": { "title": "Pick Authentication Method" } + "pick_implementation": { "title": "Pick Authentication Method" }, + "reauth_confirm": { + "title": "Re-authenticate with Spotify", + "description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}" + } }, "abort": { "already_setup": "You can only configure one Spotify account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Spotify integration is not configured. Please follow the documentation." + "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", + "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." }, "create_entry": { "default": "Successfully authenticated with Spotify." } } diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index cc10ddb887d..3b3c85dd828 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -64,7 +64,9 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): "?response_type=code&client_id=client" "&redirect_uri=https://example.com/auth/external/callback" f"&state={state}" - "&scope=user-modify-playback-state,user-read-playback-state,user-read-private" + "&scope=user-modify-playback-state,user-read-playback-state,user-read-private," + "playlist-read-private,playlist-read-collaborative,user-library-read," + "user-top-read,user-read-playback-position,user-read-recently-played,user-follow-read" ) client = await aiohttp_client(hass.http.app) @@ -83,11 +85,15 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request): ) with patch("homeassistant.components.spotify.config_flow.Spotify") as spotify_mock: - spotify_mock.return_value.current_user.return_value = {"id": "fake_id"} + spotify_mock.return_value.current_user.return_value = { + "id": "fake_id", + "display_name": "frenck", + } result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["data"]["auth_implementation"] == DOMAIN result["data"]["token"].pop("expires_at") + assert result["data"]["name"] == "frenck" assert result["data"]["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -136,3 +142,111 @@ async def test_abort_if_spotify_error( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "connection_error" + + +async def test_reauthentication(hass, aiohttp_client, aioclient_mock, current_request): + """Test Spotify reauthentication.""" + await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, + "http": {"base_url": "https://example.com"}, + }, + ) + + old_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=123, + version=1, + data={"id": "frenck", "auth_implementation": DOMAIN}, + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=old_entry.data + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + client = await aiohttp_client(hass.http.app) + await client.get(f"/auth/external/callback?code=abcd&state={state}") + + aioclient_mock.post( + "https://accounts.spotify.com/api/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.spotify.config_flow.Spotify") as spotify_mock: + spotify_mock.return_value.current_user.return_value = {"id": "frenck"} + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == DOMAIN + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + + +async def test_reauth_account_mismatch( + hass, aiohttp_client, aioclient_mock, current_request +): + """Test Spotify reauthentication with different account.""" + await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, + "http": {"base_url": "https://example.com"}, + }, + ) + + old_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=123, + version=1, + data={"id": "frenck", "auth_implementation": DOMAIN}, + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=old_entry.data + ) + + flows = hass.config_entries.flow.async_progress() + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + client = await aiohttp_client(hass.http.app) + await client.get(f"/auth/external/callback?code=abcd&state={state}") + + aioclient_mock.post( + "https://accounts.spotify.com/api/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.spotify.config_flow.Spotify") as spotify_mock: + spotify_mock.return_value.current_user.return_value = {"id": "fake_id"} + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_account_mismatch" From c2365b8c0f7e66a53bf1e11720c8c021cb6d02a5 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 27 Aug 2020 14:00:22 -0400 Subject: [PATCH 377/862] Add get_nodes command to OZW websocket api (#39317) * Add get_nodes command to OZW websocket api * Fix black * Use constants for get_nodes and get_node * Missed a couple constants --- homeassistant/components/ozw/websocket_api.py | 91 +++++++++++++++---- tests/components/ozw/test_websocket_api.py | 54 ++++++++--- 2 files changed, 115 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index 6e1a725b7a7..a71723ed086 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -17,11 +17,28 @@ ID = "id" OZW_INSTANCE = "ozw_instance" NODE_ID = "node_id" +ATTR_NODE_QUERY_STAGE = "node_query_stage" +ATTR_IS_ZWAVE_PLUS = "is_zwave_plus" +ATTR_IS_AWAKE = "is_awake" +ATTR_IS_FAILED = "is_failed" +ATTR_NODE_BAUD_RATE = "node_baud_rate" +ATTR_IS_BEAMING = "is_beaming" +ATTR_IS_FLIRS = "is_flirs" +ATTR_IS_ROUTING = "is_routing" +ATTR_IS_SECURITYV1 = "is_securityv1" +ATTR_NODE_BASIC_STRING = "node_basic_string" +ATTR_NODE_GENERIC_STRING = "node_generic_string" +ATTR_NODE_SPECIFIC_STRING = "node_specific_string" +ATTR_NODE_MANUFACTURER_NAME = "node_manufacturer_name" +ATTR_NODE_PRODUCT_NAME = "node_product_name" +ATTR_NEIGHBORS = "neighbors" + @callback def async_register_api(hass): """Register all of our api endpoints.""" websocket_api.async_register_command(hass, websocket_get_instances) + websocket_api.async_register_command(hass, websocket_get_nodes) websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_network_statistics) websocket_api.async_register_command(hass, websocket_node_metadata) @@ -45,6 +62,46 @@ def websocket_get_instances(hass, connection, msg): ) +@websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/get_nodes", + vol.Optional(OZW_INSTANCE, default=1): vol.Coerce(int), + } +) +def websocket_get_nodes(hass, connection, msg): + """Get a list of nodes for an OZW instance.""" + manager = hass.data[DOMAIN][MANAGER] + nodes = [] + + for node in manager.get_instance(msg[OZW_INSTANCE]).collections["node"]: + nodes.append( + { + ATTR_NODE_QUERY_STAGE: node.node_query_stage, + NODE_ID: node.node_id, + ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus, + ATTR_IS_AWAKE: node.is_awake, + ATTR_IS_FAILED: node.is_failed, + ATTR_NODE_BAUD_RATE: node.node_baud_rate, + ATTR_IS_BEAMING: node.is_beaming, + ATTR_IS_FLIRS: node.is_flirs, + ATTR_IS_ROUTING: node.is_routing, + ATTR_IS_SECURITYV1: node.is_securityv1, + ATTR_NODE_BASIC_STRING: node.node_basic_string, + ATTR_NODE_GENERIC_STRING: node.node_generic_string, + ATTR_NODE_SPECIFIC_STRING: node.node_specific_string, + ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name, + ATTR_NODE_PRODUCT_NAME: node.node_product_name, + ATTR_NEIGHBORS: node.neighbors, + OZW_INSTANCE: msg[OZW_INSTANCE], + } + ) + + connection.send_result( + msg[ID], + nodes, + ) + + @websocket_api.websocket_command( { vol.Required(TYPE): "ozw/network_status", @@ -96,20 +153,22 @@ def websocket_node_status(hass, connection, msg): connection.send_result( msg[ID], { - "node_query_stage": node.node_query_stage, - "node_id": node.node_id, - "is_zwave_plus": node.is_zwave_plus, - "is_awake": node.is_awake, - "is_failed": node.is_failed, - "node_baud_rate": node.node_baud_rate, - "is_beaming": node.is_beaming, - "is_flirs": node.is_flirs, - "is_routing": node.is_routing, - "is_securityv1": node.is_securityv1, - "node_basic_string": node.node_basic_string, - "node_generic_string": node.node_generic_string, - "node_specific_string": node.node_specific_string, - "neighbors": node.neighbors, + ATTR_NODE_QUERY_STAGE: node.node_query_stage, + NODE_ID: node.node_id, + ATTR_IS_ZWAVE_PLUS: node.is_zwave_plus, + ATTR_IS_AWAKE: node.is_awake, + ATTR_IS_FAILED: node.is_failed, + ATTR_NODE_BAUD_RATE: node.node_baud_rate, + ATTR_IS_BEAMING: node.is_beaming, + ATTR_IS_FLIRS: node.is_flirs, + ATTR_IS_ROUTING: node.is_routing, + ATTR_IS_SECURITYV1: node.is_securityv1, + ATTR_NODE_BASIC_STRING: node.node_basic_string, + ATTR_NODE_GENERIC_STRING: node.node_generic_string, + ATTR_NODE_SPECIFIC_STRING: node.node_specific_string, + ATTR_NODE_MANUFACTURER_NAME: node.node_manufacturer_name, + ATTR_NODE_PRODUCT_NAME: node.node_product_name, + ATTR_NEIGHBORS: node.neighbors, OZW_INSTANCE: msg[OZW_INSTANCE], }, ) @@ -152,7 +211,7 @@ def websocket_node_statistics(hass, connection, msg): connection.send_result( msg[ID], { - "node_id": msg[NODE_ID], + NODE_ID: msg[NODE_ID], "send_count": stats.send_count, "sent_failed": stats.sent_failed, "retries": stats.retries, @@ -190,7 +249,7 @@ def websocket_refresh_node_info(hass, connection, msg): forward_data = { "type": "node_updated", - "node_query_stage": node.node_query_stage, + ATTR_NODE_QUERY_STAGE: node.node_query_stage, } connection.send_message(websocket_api.event_message(msg["id"], forward_data)) diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index 7affe62cfb4..5a8e11a77c8 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -1,6 +1,24 @@ """Test OpenZWave Websocket API.""" -from homeassistant.components.ozw.websocket_api import ID, NODE_ID, OZW_INSTANCE, TYPE +from homeassistant.components.ozw.websocket_api import ( + ATTR_IS_AWAKE, + ATTR_IS_BEAMING, + ATTR_IS_FAILED, + ATTR_IS_FLIRS, + ATTR_IS_ROUTING, + ATTR_IS_SECURITYV1, + ATTR_IS_ZWAVE_PLUS, + ATTR_NEIGHBORS, + ATTR_NODE_BASIC_STRING, + ATTR_NODE_BAUD_RATE, + ATTR_NODE_GENERIC_STRING, + ATTR_NODE_QUERY_STAGE, + ATTR_NODE_SPECIFIC_STRING, + ID, + NODE_ID, + OZW_INSTANCE, + TYPE, +) from .common import MQTTMessage, setup_ozw @@ -36,19 +54,19 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): assert result[OZW_INSTANCE] == 1 assert result[NODE_ID] == 32 - assert result["node_query_stage"] == "Complete" - assert result["is_zwave_plus"] - assert result["is_awake"] - assert not result["is_failed"] - assert result["node_baud_rate"] == 100000 - assert result["is_beaming"] - assert not result["is_flirs"] - assert result["is_routing"] - assert not result["is_securityv1"] - assert result["node_basic_string"] == "Routing Slave" - assert result["node_generic_string"] == "Binary Switch" - assert result["node_specific_string"] == "Binary Power Switch" - assert result["neighbors"] == [1, 33, 36, 37, 39] + assert result[ATTR_NODE_QUERY_STAGE] == "Complete" + assert result[ATTR_IS_ZWAVE_PLUS] + assert result[ATTR_IS_AWAKE] + assert not result[ATTR_IS_FAILED] + assert result[ATTR_NODE_BAUD_RATE] == 100000 + assert result[ATTR_IS_BEAMING] + assert not result[ATTR_IS_FLIRS] + assert result[ATTR_IS_ROUTING] + assert not result[ATTR_IS_SECURITYV1] + assert result[ATTR_NODE_BASIC_STRING] == "Routing Slave" + assert result[ATTR_NODE_GENERIC_STRING] == "Binary Switch" + assert result[ATTR_NODE_SPECIFIC_STRING] == "Binary Power Switch" + assert result[ATTR_NEIGHBORS] == [1, 33, 36, 37, 39] # Test node statistics await client.send_json({ID: 7, TYPE: "ozw/node_statistics", NODE_ID: 39}) @@ -82,6 +100,14 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): assert result[OZW_INSTANCE] == 1 assert result["node_count"] == 5 + # Test get nodes + await client.send_json({ID: 10, TYPE: "ozw/get_nodes"}) + msg = await client.receive_json() + result = msg["result"] + assert len(result) == 5 + assert result[2][ATTR_IS_AWAKE] + assert not result[1][ATTR_IS_FAILED] + async def test_refresh_node(hass, generic_data, sent_messages, hass_ws_client): """Test the ozw refresh node api.""" From 4e39a00b3df27dc17ef02e4dfa96639a6d6ab2f2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Aug 2020 21:04:58 +0200 Subject: [PATCH 378/862] Use boolean for mqtt fan state (#39332) --- homeassistant/components/mqtt/fan.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index b2836112921..e70ff62b0fb 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -22,8 +22,6 @@ from homeassistant.const import ( CONF_PAYLOAD_ON, CONF_STATE, CONF_UNIQUE_ID, - STATE_OFF, - STATE_ON, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -155,7 +153,7 @@ class MqttFan( def __init__(self, config, config_entry, discovery_data): """Initialize the MQTT fan.""" self._unique_id = config.get(CONF_UNIQUE_ID) - self._state = STATE_OFF + self._state = False self._speed = None self._oscillation = None self._supported_features = 0 @@ -257,9 +255,9 @@ class MqttFan( """Handle new received MQTT message.""" payload = templates[CONF_STATE](msg.payload) if payload == self._payload["STATE_ON"]: - self._state = STATE_ON + self._state = True elif payload == self._payload["STATE_OFF"]: - self._state = STATE_OFF + self._state = False self.async_write_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: @@ -337,7 +335,7 @@ class MqttFan( @property def is_on(self): """Return true if device is on.""" - return self._state == STATE_ON + return self._state @property def name(self) -> str: @@ -379,7 +377,7 @@ class MqttFan( if speed: await self.async_set_speed(speed) if self._optimistic: - self._state = STATE_ON + self._state = True self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: @@ -395,7 +393,7 @@ class MqttFan( self._config[CONF_RETAIN], ) if self._optimistic: - self._state = STATE_OFF + self._state = False self.async_write_ha_state() async def async_set_speed(self, speed: str) -> None: From 90ac426a54d2f617aeccdc261bbe6a878c88865c Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Thu, 27 Aug 2020 21:08:39 +0200 Subject: [PATCH 379/862] Meteo france "next_rain" attributes rework (#39092) * Improve next_rain sensor attributes * Add log message to identify missing condition * Add a condtion * Set the coordinator for 1 hour rain forecast in English * Attribut dict keys shorten * reverse transalate the API results for next rain * Use f string for the keys in the dict * Remove Logging from state property * Remove other logging from state property --- homeassistant/components/meteo_france/const.py | 2 ++ homeassistant/components/meteo_france/sensor.py | 9 ++++++--- homeassistant/components/meteo_france/weather.py | 3 --- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 8e6c625e331..fb960b3b26a 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -24,6 +24,7 @@ FORECAST_MODE_DAILY = "daily" FORECAST_MODE = [FORECAST_MODE_HOURLY, FORECAST_MODE_DAILY] ATTR_NEXT_RAIN_1_HOUR_FORECAST = "1_hour_forecast" +ATTR_NEXT_RAIN_DT_REF = "forecast_time_ref" ENTITY_NAME = "name" ENTITY_UNIT = "unit" @@ -150,6 +151,7 @@ CONDITION_CLASSES = { "Pluies éparses / Rares averses", "Pluies éparses", "Rares averses", + "Pluie modérée", "Pluie / Averses", "Averses", "Pluie", diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 178df5c992f..de767217cbf 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -15,6 +15,7 @@ from homeassistant.util import dt as dt_util from .const import ( ATTR_NEXT_RAIN_1_HOUR_FORECAST, + ATTR_NEXT_RAIN_DT_REF, ATTRIBUTION, COORDINATOR_ALERT, COORDINATOR_FORECAST, @@ -185,11 +186,13 @@ class MeteoFranceRainSensor(MeteoFranceSensor): @property def device_state_attributes(self): """Return the state attributes.""" + reference_dt = self.coordinator.data.forecast[0]["dt"] return { - ATTR_NEXT_RAIN_1_HOUR_FORECAST: [ - {dt_util.utc_from_timestamp(item["dt"]).isoformat(): item["desc"]} + ATTR_NEXT_RAIN_DT_REF: dt_util.utc_from_timestamp(reference_dt).isoformat(), + ATTR_NEXT_RAIN_1_HOUR_FORECAST: { + f"{int((item['dt'] - reference_dt) / 60)} min": item["desc"] for item in self.coordinator.data.forecast - ], + }, ATTR_ATTRIBUTION: ATTRIBUTION, } diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 6f110dbcdeb..30f3a350299 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -130,9 +130,6 @@ class MeteoFranceWeather(WeatherEntity): for forecast in self.coordinator.data.forecast: # Can have data in the past if forecast["dt"] < today: - _LOGGER.debug( - "remove forecast in the past: %s %s", self._mode, forecast - ) continue forecast_data.append( { From f4f8aa3e52f5cc52d8f0f1b1f2e704fa9e5c69ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Aug 2020 14:17:52 -0500 Subject: [PATCH 380/862] Prevent duckdns from consuming 100% cpu when time abruptly moves forward (#39334) --- homeassistant/components/duckdns/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index b3da1ec2752..3fd1e4e4d5e 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -120,7 +120,10 @@ def async_track_time_interval_backoff(hass, action, intervals) -> CALLBACK_TYPE: failed = 0 finally: delay = intervals[failed] if failed < len(intervals) else intervals[-1] - remove = async_track_point_in_utc_time(hass, interval_listener, now + delay) + # call dt_util.utcnow() again in case time abruptly moves forward + remove = async_track_point_in_utc_time( + hass, interval_listener, dt_util.utcnow() + delay + ) hass.async_run_job(interval_listener, dt_util.utcnow()) From b1444ffefb47c99fddf2a25b96de2b4f0313faa0 Mon Sep 17 00:00:00 2001 From: Paul Daumlechner Date: Thu, 27 Aug 2020 21:47:15 +0200 Subject: [PATCH 381/862] Bump zeroconf to 0.28.2 (#39322) * Bump zeroconf to 0.28.2 * Requirements updated --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 883188b31cf..79bef3bf956 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.1"], + "requirements": ["zeroconf==0.28.2"], "dependencies": ["api"], "codeowners": ["@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 757f1b56fe6..60d40d73646 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ sqlalchemy==1.3.19 voluptuous-serialize==2.4.0 voluptuous==0.11.7 yarl==1.4.2 -zeroconf==0.28.1 +zeroconf==0.28.2 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index d6cd0e25a35..36ee5ea94be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2276,7 +2276,7 @@ youtube_dl==2020.07.28 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.1 +zeroconf==0.28.2 # homeassistant.components.zha zha-quirks==0.0.43 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d4025ebef3a..2a08eaef6b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1037,7 +1037,7 @@ xmltodict==0.12.0 yeelight==0.5.2 # homeassistant.components.zeroconf -zeroconf==0.28.1 +zeroconf==0.28.2 # homeassistant.components.zha zha-quirks==0.0.43 From ca05f8928d9bf3690a89a4a0ca22d1054b5eba85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Aug 2020 17:10:02 -0500 Subject: [PATCH 382/862] Switch duckdns to use async_call_later (#39339) --- homeassistant/components/duckdns/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 3fd1e4e4d5e..3e08d0e7b34 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -9,7 +9,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -120,10 +120,7 @@ def async_track_time_interval_backoff(hass, action, intervals) -> CALLBACK_TYPE: failed = 0 finally: delay = intervals[failed] if failed < len(intervals) else intervals[-1] - # call dt_util.utcnow() again in case time abruptly moves forward - remove = async_track_point_in_utc_time( - hass, interval_listener, dt_util.utcnow() + delay - ) + remove = async_call_later(hass, delay.total_seconds(), interval_listener) hass.async_run_job(interval_listener, dt_util.utcnow()) From 68ba1d8790399061e62712ac5d16478e3c23c391 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 28 Aug 2020 00:05:07 +0000 Subject: [PATCH 383/862] [ci skip] Translation update --- homeassistant/components/media_player/translations/ko.json | 2 +- homeassistant/components/spotify/translations/ca.json | 7 ++++++- homeassistant/components/spotify/translations/en.json | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/translations/ko.json b/homeassistant/components/media_player/translations/ko.json index 47e019a879d..e727e744d73 100644 --- a/homeassistant/components/media_player/translations/ko.json +++ b/homeassistant/components/media_player/translations/ko.json @@ -18,5 +18,5 @@ "standby": "\uc900\ube44\uc911" } }, - "title": "\ubbf8\ub514\uc5b4\uc7ac\uc0dd\uae30" + "title": "\ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4" } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ca.json b/homeassistant/components/spotify/translations/ca.json index 887d3b7236c..005e0c5c331 100644 --- a/homeassistant/components/spotify/translations/ca.json +++ b/homeassistant/components/spotify/translations/ca.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Spotify.", "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "La integraci\u00f3 Spotify no est\u00e0 configurada. Mira'n la documentaci\u00f3." + "missing_configuration": "La integraci\u00f3 Spotify no est\u00e0 configurada. Mira'n la documentaci\u00f3.", + "reauth_account_mismatch": "El compte Spotify autenticat, no coincideix amb el compte necessari per a la re-autenticaci\u00f3." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Spotify." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 de Spotify ha de tornar a autenticar-se amb el compte de Spotify: {account}", + "title": "Re-autenticaci\u00f3 amb Spotify" } } } diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json index c869d06cab3..6cdafbf061c 100644 --- a/homeassistant/components/spotify/translations/en.json +++ b/homeassistant/components/spotify/translations/en.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "You can only configure one Spotify account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Spotify integration is not configured. Please follow the documentation." + "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", + "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." }, "create_entry": { "default": "Successfully authenticated with Spotify." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Spotify integration needs to re-authenticate with Spotify for account: {account}", + "title": "Re-authenticate with Spotify" } } } From a83c778c4fdc787996e713bbd456b404c869005d Mon Sep 17 00:00:00 2001 From: jgrob1 Date: Fri, 28 Aug 2020 02:35:33 +0100 Subject: [PATCH 384/862] Bump rflink to 0.0.54 (#39342) * Update manifest.json * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/rflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index e9ddd17e60a..266092581f0 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -2,6 +2,6 @@ "domain": "rflink", "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": ["rflink==0.0.52"], + "requirements": ["rflink==0.0.54"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 36ee5ea94be..1e7f3270e43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1887,7 +1887,7 @@ restrictedpython==5.0 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.52 +rflink==0.0.54 # homeassistant.components.ring ring_doorbell==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a08eaef6b3..17c7385ff29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -872,7 +872,7 @@ regenmaschine==2.1.0 restrictedpython==5.0 # homeassistant.components.rflink -rflink==0.0.52 +rflink==0.0.54 # homeassistant.components.ring ring_doorbell==0.6.0 From 24db31fa286d94810d2118ebaacf100f0761d162 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 28 Aug 2020 04:39:27 +0300 Subject: [PATCH 385/862] Add (un)bypass services to Risco (#39292) * Add (un)bypass services to Risco * Simplify service registration --- .../components/risco/binary_sensor.py | 23 ++++++++++++++++ homeassistant/components/risco/manifest.json | 2 +- homeassistant/components/risco/services.yaml | 15 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/risco/test_binary_sensor.py | 26 +++++++++++++++++++ 6 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/risco/services.yaml diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index 978e6d11eb6..f3c35071111 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -3,13 +3,23 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, BinarySensorEntity, ) +from homeassistant.helpers import entity_platform from .const import DATA_COORDINATOR, DOMAIN from .entity import RiscoEntity +SERVICE_BYPASS_ZONE = "bypass_zone" +SERVICE_UNBYPASS_ZONE = "unbypass_zone" + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Risco alarm control panel.""" + platform = entity_platform.current_platform.get() + platform.async_register_entity_service(SERVICE_BYPASS_ZONE, {}, "async_bypass_zone") + platform.async_register_entity_service( + SERVICE_UNBYPASS_ZONE, {}, "async_unbypass_zone" + ) + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] entities = [ RiscoBinarySensor(coordinator, zone_id, zone) @@ -64,3 +74,16 @@ class RiscoBinarySensor(BinarySensorEntity, RiscoEntity): def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" return DEVICE_CLASS_MOTION + + async def _bypass(self, bypass): + alarm = await self._risco.bypass_zone(self._zone_id, bypass) + self._zone = alarm.zones[self._zone_id] + self.async_write_ha_state() + + async def async_bypass_zone(self): + """Bypass this zone.""" + await self._bypass(True) + + async def async_unbypass_zone(self): + """Unbypass this zone.""" + await self._bypass(False) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index af8bdc960f2..2e360cd0c43 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", "requirements": [ - "pyrisco==0.2.1" + "pyrisco==0.2.3" ], "codeowners": [ "@OnFreund" diff --git a/homeassistant/components/risco/services.yaml b/homeassistant/components/risco/services.yaml new file mode 100644 index 00000000000..8b6c8c06f01 --- /dev/null +++ b/homeassistant/components/risco/services.yaml @@ -0,0 +1,15 @@ +# Describes the format for available Risco services + +bypass_zone: + description: Bypass a Risco Zone + fields: + entity_id: + description: Entity ID of the zone to bypass + example: "binary_sensor.living_room_motion" + +unbypass_zone: + description: Unbypass a Risco Zone + fields: + entity_id: + description: Entity ID of the zone to unbypass + example: "binary_sensor.living_room_motion" diff --git a/requirements_all.txt b/requirements_all.txt index 1e7f3270e43..af78e8aeaf6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1582,7 +1582,7 @@ pyrecswitch==1.0.2 pyrepetier==3.0.5 # homeassistant.components.risco -pyrisco==0.2.1 +pyrisco==0.2.3 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17c7385ff29..4356d0bc2a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -757,7 +757,7 @@ pyps4-2ndscreen==1.1.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.2.1 +pyrisco==0.2.3 # homeassistant.components.acer_projector # homeassistant.components.zha diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 689b2e17c28..ab7934e523c 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -163,3 +163,29 @@ async def test_states(hass, two_zone_alarm): await _check_state(hass, two_zone_alarm, True, False, SECOND_ENTITY_ID, 1) await _check_state(hass, two_zone_alarm, False, True, SECOND_ENTITY_ID, 1) await _check_state(hass, two_zone_alarm, False, False, SECOND_ENTITY_ID, 1) + + +async def test_bypass(hass, two_zone_alarm): + """Test bypassing a zone.""" + await _setup_risco(hass) + with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + DOMAIN, "bypass_zone", service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(0, True) + + +async def test_unbypass(hass, two_zone_alarm): + """Test unbypassing a zone.""" + await _setup_risco(hass) + with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + DOMAIN, "unbypass_zone", service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(0, False) From b14af3e727803bb6a6f2715e06ee689b277d8164 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 28 Aug 2020 05:23:01 +0300 Subject: [PATCH 386/862] Add Custom mapping of Risco states (#39218) * Custom mapping of Risco states * More informative error log * Add alternative Risco terms * Black formatting --- homeassistant/components/risco/__init__.py | 2 +- .../components/risco/alarm_control_panel.py | 102 +++++--- homeassistant/components/risco/config_flow.py | 84 ++++++- homeassistant/components/risco/const.py | 33 +++ homeassistant/components/risco/strings.json | 22 ++ .../risco/test_alarm_control_panel.py | 231 ++++++++++++++---- tests/components/risco/test_config_flow.py | 101 ++++++-- 7 files changed, 464 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index bfdf322d4d5..09995f585d6 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -104,4 +104,4 @@ class RiscoDataUpdateCoordinator(DataUpdateCoordinator): try: return await self.risco.get_state() except (CannotConnectError, UnauthorizedError, OperationError) as error: - raise UpdateFailed from error + raise UpdateFailed(error) from error diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 2484772d5f7..e6548c2ffdc 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -7,12 +7,16 @@ from homeassistant.components.alarm_control_panel import ( ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, ) from homeassistant.const import ( CONF_PIN, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, @@ -21,29 +25,33 @@ from homeassistant.const import ( from .const import ( CONF_CODE_ARM_REQUIRED, CONF_CODE_DISARM_REQUIRED, + CONF_HA_STATES_TO_RISCO, + CONF_RISCO_STATES_TO_HA, DATA_COORDINATOR, + DEFAULT_OPTIONS, DOMAIN, + RISCO_ARM, + RISCO_GROUPS, + RISCO_PARTIAL_ARM, ) from .entity import RiscoEntity _LOGGER = logging.getLogger(__name__) -SUPPORTED_STATES = [ - STATE_ALARM_DISARMED, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_TRIGGERED, -] +STATES_TO_SUPPORTED_FEATURES = { + STATE_ALARM_ARMED_AWAY: SUPPORT_ALARM_ARM_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS: SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME: SUPPORT_ALARM_ARM_HOME, + STATE_ALARM_ARMED_NIGHT: SUPPORT_ALARM_ARM_NIGHT, +} async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Risco alarm control panel.""" coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - code = config_entry.data[CONF_PIN] - code_arm_req = config_entry.options.get(CONF_CODE_ARM_REQUIRED, False) - code_disarm_req = config_entry.options.get(CONF_CODE_DISARM_REQUIRED, False) + options = {**DEFAULT_OPTIONS, **config_entry.options} entities = [ - RiscoAlarm(coordinator, partition_id, code, code_arm_req, code_disarm_req) + RiscoAlarm(coordinator, partition_id, config_entry.data[CONF_PIN], options) for partition_id in coordinator.data.partitions ] @@ -53,16 +61,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): """Representation of a Risco partition.""" - def __init__( - self, coordinator, partition_id, code, code_arm_required, code_disarm_required - ): + def __init__(self, coordinator, partition_id, code, options): """Init the partition.""" super().__init__(coordinator) self._partition_id = partition_id self._partition = self._coordinator.data.partitions[self._partition_id] self._code = code - self._code_arm_required = code_arm_required - self._code_disarm_required = code_disarm_required + self._code_arm_required = options[CONF_CODE_ARM_REQUIRED] + self._code_disarm_required = options[CONF_CODE_DISARM_REQUIRED] + self._risco_to_ha = options[CONF_RISCO_STATES_TO_HA] + self._ha_to_risco = options[CONF_HA_STATES_TO_RISCO] + self._supported_states = 0 + for state in self._ha_to_risco: + self._supported_states |= STATES_TO_SUPPORTED_FEATURES[state] def _get_data_from_coordinator(self): self._partition = self._coordinator.data.partitions[self._partition_id] @@ -93,19 +104,23 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): return STATE_ALARM_TRIGGERED if self._partition.arming: return STATE_ALARM_ARMING - if self._partition.armed: - return STATE_ALARM_ARMED_AWAY - if self._partition.partially_armed: - return STATE_ALARM_ARMED_HOME if self._partition.disarmed: return STATE_ALARM_DISARMED + if self._partition.armed: + return self._risco_to_ha[RISCO_ARM] + if self._partition.partially_armed: + for group, armed in self._partition.groups.items(): + if armed: + return self._risco_to_ha[group] + + return self._risco_to_ha[RISCO_PARTIAL_ARM] return None @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + return self._supported_states @property def code_arm_required(self): @@ -117,32 +132,49 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): """Return one or more digits/characters.""" return FORMAT_NUMBER - def _validate_code(self, code, state): + def _validate_code(self, code): """Validate given code.""" - check = code == self._code - if not check: - _LOGGER.warning("Wrong code entered for %s", state) - return check + return code == self._code async def async_alarm_disarm(self, code=None): """Send disarm command.""" - if self._code_disarm_required and not self._validate_code(code, "disarming"): + if self._code_disarm_required and not self._validate_code(code): + _LOGGER.warning("Wrong code entered for disarming") return await self._call_alarm_method("disarm") async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - if self._code_arm_required and not self._validate_code(code, "arming home"): - return - await self._call_alarm_method("partial_arm") + await self._arm(STATE_ALARM_ARMED_HOME, code) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - if self._code_arm_required and not self._validate_code(code, "arming away"): - return - await self._call_alarm_method("arm") + await self._arm(STATE_ALARM_ARMED_AWAY, code) - async def _call_alarm_method(self, method): - alarm_obj = await getattr(self._risco, method)(self._partition_id) - self._partition = alarm_obj.partitions[self._partition_id] + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + await self._arm(STATE_ALARM_ARMED_NIGHT, code) + + async def async_alarm_arm_custom_bypass(self, code=None): + """Send arm custom bypass command.""" + await self._arm(STATE_ALARM_ARMED_CUSTOM_BYPASS, code) + + async def _arm(self, mode, code): + if self._code_arm_required and not self._validate_code(code): + _LOGGER.warning("Wrong code entered for %s", mode) + return + + risco_state = self._ha_to_risco[mode] + if not risco_state: + _LOGGER.warning("No mapping for mode %s", mode) + return + + if risco_state in RISCO_GROUPS: + await self._call_alarm_method("group_arm", risco_state) + else: + await self._call_alarm_method(risco_state) + + async def _call_alarm_method(self, method, *args): + alarm = await getattr(self._risco, method)(self._partition_id, *args) + self._partition = alarm.partitions[self._partition_id] self.async_write_ha_state() diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 03fbc322075..507e3943f2d 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -10,13 +10,20 @@ from homeassistant.const import ( CONF_PIN, CONF_SCAN_INTERVAL, CONF_USERNAME, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( CONF_CODE_ARM_REQUIRED, CONF_CODE_DISARM_REQUIRED, - DEFAULT_SCAN_INTERVAL, + CONF_HA_STATES_TO_RISCO, + CONF_RISCO_STATES_TO_HA, + DEFAULT_OPTIONS, + RISCO_STATES, ) from .const import DOMAIN # pylint:disable=unused-import @@ -24,6 +31,12 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str, CONF_PIN: str}) +HA_STATES = [ + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS, +] async def validate_input(hass: core.HomeAssistant, data): @@ -83,22 +96,20 @@ class RiscoOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize.""" self.config_entry = config_entry + self._data = {**DEFAULT_OPTIONS, **config_entry.options} def _options_schema(self): - scan_interval = self.config_entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ) - code_arm_required = self.config_entry.options.get(CONF_CODE_ARM_REQUIRED, False) - code_disarm_required = self.config_entry.options.get( - CONF_CODE_DISARM_REQUIRED, False - ) - return vol.Schema( { - vol.Required(CONF_SCAN_INTERVAL, default=scan_interval): int, - vol.Required(CONF_CODE_ARM_REQUIRED, default=code_arm_required): bool, vol.Required( - CONF_CODE_DISARM_REQUIRED, default=code_disarm_required + CONF_SCAN_INTERVAL, default=self._data[CONF_SCAN_INTERVAL] + ): int, + vol.Required( + CONF_CODE_ARM_REQUIRED, default=self._data[CONF_CODE_ARM_REQUIRED] + ): bool, + vol.Required( + CONF_CODE_DISARM_REQUIRED, + default=self._data[CONF_CODE_DISARM_REQUIRED], ): bool, } ) @@ -106,6 +117,53 @@ class RiscoOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + self._data = {**self._data, **user_input} + return await self.async_step_risco_to_ha() return self.async_show_form(step_id="init", data_schema=self._options_schema()) + + async def async_step_risco_to_ha(self, user_input=None): + """Map Risco states to HA states.""" + if user_input is not None: + self._data[CONF_RISCO_STATES_TO_HA] = user_input + return await self.async_step_ha_to_risco() + + risco_to_ha = self._data[CONF_RISCO_STATES_TO_HA] + options = vol.Schema( + { + vol.Required(risco_state, default=risco_to_ha[risco_state]): vol.In( + HA_STATES + ) + for risco_state in RISCO_STATES + } + ) + + return self.async_show_form(step_id="risco_to_ha", data_schema=options) + + async def async_step_ha_to_risco(self, user_input=None): + """Map HA states to Risco states.""" + if user_input is not None: + self._data[CONF_HA_STATES_TO_RISCO] = user_input + return self.async_create_entry(title="", data=self._data) + + options = {} + risco_to_ha = self._data[CONF_RISCO_STATES_TO_HA] + # we iterate over HA_STATES, instead of set(self._risco_to_ha.values()) + # to ensure a consistent order + for ha_state in HA_STATES: + if ha_state not in risco_to_ha.values(): + continue + + values = [ + risco_state + for risco_state in RISCO_STATES + if risco_to_ha[risco_state] == ha_state + ] + current = self._data[CONF_HA_STATES_TO_RISCO].get(ha_state) + if current not in values: + current = values[0] + options[vol.Required(ha_state, default=current)] = vol.In(values) + + return self.async_show_form( + step_id="ha_to_risco", data_schema=vol.Schema(options) + ) diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index 23d29bc11a9..f66f0d33000 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -1,5 +1,11 @@ """Constants for the Risco integration.""" +from homeassistant.const import ( + CONF_SCAN_INTERVAL, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, +) + DOMAIN = "risco" DATA_COORDINATOR = "risco" @@ -8,3 +14,30 @@ DEFAULT_SCAN_INTERVAL = 30 CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_DISARM_REQUIRED = "code_disarm_required" +CONF_RISCO_STATES_TO_HA = "risco_states_to_ha" +CONF_HA_STATES_TO_RISCO = "ha_states_to_risco" + +RISCO_GROUPS = ["A", "B", "C", "D"] +RISCO_ARM = "arm" +RISCO_PARTIAL_ARM = "partial_arm" +RISCO_STATES = [RISCO_ARM, RISCO_PARTIAL_ARM, *RISCO_GROUPS] + +DEFAULT_RISCO_GROUPS_TO_HA = {group: STATE_ALARM_ARMED_HOME for group in RISCO_GROUPS} +DEFAULT_RISCO_STATES_TO_HA = { + RISCO_ARM: STATE_ALARM_ARMED_AWAY, + RISCO_PARTIAL_ARM: STATE_ALARM_ARMED_HOME, + **DEFAULT_RISCO_GROUPS_TO_HA, +} + +DEFAULT_HA_STATES_TO_RISCO = { + STATE_ALARM_ARMED_AWAY: RISCO_ARM, + STATE_ALARM_ARMED_HOME: RISCO_PARTIAL_ARM, +} + +DEFAULT_OPTIONS = { + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_CODE_ARM_REQUIRED: False, + CONF_CODE_DISARM_REQUIRED: False, + CONF_RISCO_STATES_TO_HA: DEFAULT_RISCO_STATES_TO_HA, + CONF_HA_STATES_TO_RISCO: DEFAULT_HA_STATES_TO_RISCO, +} diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json index 32f3334d7ed..dc6031e0ad3 100644 --- a/homeassistant/components/risco/strings.json +++ b/homeassistant/components/risco/strings.json @@ -27,6 +27,28 @@ "code_arm_required": "Require pin code to arm", "code_disarm_required": "Require pin code to disarm" } + }, + "risco_to_ha": { + "title": "Map Risco states to Home Assistant states", + "description": "Select what state your Home Assistant alarm will report for every state reported by Risco", + "data": { + "arm": "Armed (AWAY)", + "partial_arm": "Partially Armed (STAY)", + "A": "Group A", + "B": "Group B", + "C": "Group C", + "D": "Group D" + } + }, + "ha_to_risco": { + "title": "Map Home Assistant states to Risco states", + "description": "Select what state to set your Risco alarm to when arming the Home Assistant alarm", + "data": { + "armed_away": "Armed Away", + "armed_home": "Armed Home", + "armed_night": "Armed Night", + "armed_custom_bypass": "Armed Custom Bypass" + } } } } diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 75038eef377..424699cbb4c 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -2,6 +2,12 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.components.risco import CannotConnectError, UnauthorizedError from homeassistant.components.risco.const import DOMAIN from homeassistant.const import ( @@ -9,10 +15,14 @@ from homeassistant.const import ( CONF_PIN, CONF_USERNAME, SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, @@ -34,6 +44,40 @@ FIRST_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0" SECOND_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_1" CODES_REQUIRED_OPTIONS = {"code_arm_required": True, "code_disarm_required": True} +TEST_RISCO_TO_HA = { + "arm": STATE_ALARM_ARMED_AWAY, + "partial_arm": STATE_ALARM_ARMED_HOME, + "A": STATE_ALARM_ARMED_HOME, + "B": STATE_ALARM_ARMED_HOME, + "C": STATE_ALARM_ARMED_NIGHT, + "D": STATE_ALARM_ARMED_NIGHT, +} +TEST_FULL_RISCO_TO_HA = { + **TEST_RISCO_TO_HA, + "D": STATE_ALARM_ARMED_CUSTOM_BYPASS, +} +TEST_HA_TO_RISCO = { + STATE_ALARM_ARMED_AWAY: "arm", + STATE_ALARM_ARMED_HOME: "partial_arm", + STATE_ALARM_ARMED_NIGHT: "C", +} +TEST_FULL_HA_TO_RISCO = { + **TEST_HA_TO_RISCO, + STATE_ALARM_ARMED_CUSTOM_BYPASS: "D", +} +CUSTOM_MAPPING_OPTIONS = { + "risco_states_to_ha": TEST_RISCO_TO_HA, + "ha_states_to_risco": TEST_HA_TO_RISCO, +} + +FULL_CUSTOM_MAPPING = { + "risco_states_to_ha": TEST_FULL_RISCO_TO_HA, + "ha_states_to_risco": TEST_FULL_HA_TO_RISCO, +} + +EXPECTED_FEATURES = ( + SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT +) def _partition_mock(): @@ -152,55 +196,68 @@ async def _check_state(hass, alarm, property, state, entity_id, partition_id): async def test_states(hass, two_part_alarm): """Test the various alarm states.""" - await _setup_risco(hass) + await _setup_risco(hass, CUSTOM_MAPPING_OPTIONS) assert hass.states.get(FIRST_ENTITY_ID).state == STATE_UNKNOWN - await _check_state( - hass, two_part_alarm, "triggered", STATE_ALARM_TRIGGERED, FIRST_ENTITY_ID, 0 - ) - await _check_state( - hass, two_part_alarm, "triggered", STATE_ALARM_TRIGGERED, SECOND_ENTITY_ID, 1 - ) - await _check_state( - hass, two_part_alarm, "arming", STATE_ALARM_ARMING, FIRST_ENTITY_ID, 0 - ) - await _check_state( - hass, two_part_alarm, "arming", STATE_ALARM_ARMING, SECOND_ENTITY_ID, 1 - ) - await _check_state( - hass, two_part_alarm, "armed", STATE_ALARM_ARMED_AWAY, FIRST_ENTITY_ID, 0 - ) - await _check_state( - hass, two_part_alarm, "armed", STATE_ALARM_ARMED_AWAY, SECOND_ENTITY_ID, 1 - ) - await _check_state( - hass, - two_part_alarm, - "partially_armed", - STATE_ALARM_ARMED_HOME, - FIRST_ENTITY_ID, - 0, - ) - await _check_state( - hass, - two_part_alarm, - "partially_armed", - STATE_ALARM_ARMED_HOME, - SECOND_ENTITY_ID, - 1, - ) - await _check_state( - hass, two_part_alarm, "disarmed", STATE_ALARM_DISARMED, FIRST_ENTITY_ID, 0 - ) - await _check_state( - hass, two_part_alarm, "disarmed", STATE_ALARM_DISARMED, SECOND_ENTITY_ID, 1 - ) + for partition_id, entity_id in {0: FIRST_ENTITY_ID, 1: SECOND_ENTITY_ID}.items(): + await _check_state( + hass, + two_part_alarm, + "triggered", + STATE_ALARM_TRIGGERED, + entity_id, + partition_id, + ) + await _check_state( + hass, two_part_alarm, "arming", STATE_ALARM_ARMING, entity_id, partition_id + ) + await _check_state( + hass, + two_part_alarm, + "armed", + STATE_ALARM_ARMED_AWAY, + entity_id, + partition_id, + ) + await _check_state( + hass, + two_part_alarm, + "partially_armed", + STATE_ALARM_ARMED_HOME, + entity_id, + partition_id, + ) + await _check_state( + hass, + two_part_alarm, + "disarmed", + STATE_ALARM_DISARMED, + entity_id, + partition_id, + ) + + groups = {"A": False, "B": False, "C": True, "D": False} + with patch.object( + two_part_alarm.partitions[partition_id], + "groups", + new_callable=PropertyMock(return_value=groups), + ): + await _check_state( + hass, + two_part_alarm, + "partially_armed", + STATE_ALARM_ARMED_NIGHT, + entity_id, + partition_id, + ) -async def _test_service_call(hass, service, method, entity_id, partition_id, **kwargs): +async def _test_service_call( + hass, service, method, entity_id, partition_id, *args, **kwargs +): with patch(f"homeassistant.components.risco.RiscoAPI.{method}") as set_mock: await _call_alarm_service(hass, service, entity_id, **kwargs) - set_mock.assert_awaited_once_with(partition_id) + set_mock.assert_awaited_once_with(partition_id, *args) async def _test_no_service_call( @@ -219,9 +276,13 @@ async def _call_alarm_service(hass, service, entity_id, **kwargs): ) -async def test_sets(hass, two_part_alarm): - """Test settings the various modes.""" - await _setup_risco(hass) +async def test_sets_custom_mapping(hass, two_part_alarm): + """Test settings the various modes when mapping some states.""" + await _setup_risco(hass, CUSTOM_MAPPING_OPTIONS) + + registry = await hass.helpers.entity_registry.async_get_registry() + entity = registry.async_get(FIRST_ENTITY_ID) + assert entity.supported_features == EXPECTED_FEATURES await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0) await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1) @@ -233,11 +294,51 @@ async def test_sets(hass, two_part_alarm): await _test_service_call( hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1 ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, "C" + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C" + ) + + +async def test_sets_full_custom_mapping(hass, two_part_alarm): + """Test settings the various modes when mapping all states.""" + await _setup_risco(hass, FULL_CUSTOM_MAPPING) + + registry = await hass.helpers.entity_registry.async_get_registry() + entity = registry.async_get(FIRST_ENTITY_ID) + assert ( + entity.supported_features == EXPECTED_FEATURES | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + ) + + await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0) + await _test_service_call(hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1) + await _test_service_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0) + await _test_service_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1) + await _test_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0 + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1 + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, "C" + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C" + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", FIRST_ENTITY_ID, 0, "D" + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", SECOND_ENTITY_ID, 1, "D" + ) async def test_sets_with_correct_code(hass, two_part_alarm): """Test settings the various modes when code is required.""" - await _setup_risco(hass, CODES_REQUIRED_OPTIONS) + await _setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) code = {"code": 1234} await _test_service_call( @@ -258,11 +359,28 @@ async def test_sets_with_correct_code(hass, two_part_alarm): await _test_service_call( hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1, **code ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, "C", **code + ) + await _test_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C", **code + ) + await _test_no_service_call( + hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", FIRST_ENTITY_ID, 0, **code + ) + await _test_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + SECOND_ENTITY_ID, + 1, + **code, + ) async def test_sets_with_incorrect_code(hass, two_part_alarm): """Test settings the various modes when code is required and incorrect.""" - await _setup_risco(hass, CODES_REQUIRED_OPTIONS) + await _setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) code = {"code": 4321} await _test_no_service_call( @@ -283,3 +401,20 @@ async def test_sets_with_incorrect_code(hass, two_part_alarm): await _test_no_service_call( hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1, **code ) + await _test_no_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, **code + ) + await _test_no_service_call( + hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, **code + ) + await _test_no_service_call( + hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", FIRST_ENTITY_ID, 0, **code + ) + await _test_no_service_call( + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "partial_arm", + SECOND_ENTITY_ID, + 1, + **code, + ) diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index ae6e34e6f60..47fd0927cb1 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -1,4 +1,7 @@ """Test the Risco config flow.""" +import pytest +import voluptuous as vol + from homeassistant import config_entries, data_entry_flow from homeassistant.components.risco.config_flow import ( CannotConnectError, @@ -16,6 +19,27 @@ TEST_DATA = { "pin": "1234", } +TEST_RISCO_TO_HA = { + "arm": "armed_away", + "partial_arm": "armed_home", + "A": "armed_home", + "B": "armed_home", + "C": "armed_night", + "D": "armed_night", +} + +TEST_HA_TO_RISCO = { + "armed_away": "arm", + "armed_home": "partial_arm", + "armed_night": "C", +} + +TEST_OPTIONS = { + "scan_interval": 10, + "code_arm_required": True, + "code_disarm_required": True, +} + async def test_form(hass): """Test we get the form.""" @@ -133,12 +157,6 @@ async def test_form_already_exists(hass): async def test_options_flow(hass): """Test options flow.""" - conf = { - "scan_interval": 10, - "code_arm_required": True, - "code_disarm_required": True, - } - entry = MockConfigEntry( domain=DOMAIN, unique_id=TEST_DATA["username"], @@ -147,16 +165,71 @@ async def test_options_flow(hass): entry.add_to_hass(hass) + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=TEST_OPTIONS, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "risco_to_ha" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=TEST_RISCO_TO_HA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "ha_to_risco" + with patch("homeassistant.components.risco.async_setup_entry", return_value=True): - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input=conf, + user_input=TEST_HA_TO_RISCO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert entry.options == conf + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == { + **TEST_OPTIONS, + "risco_states_to_ha": TEST_RISCO_TO_HA, + "ha_states_to_risco": TEST_HA_TO_RISCO, + } + + +async def test_ha_to_risco_schema(hass): + """Test that the schema for the ha-to-risco mapping step is generated properly.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_DATA["username"], + data=TEST_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=TEST_OPTIONS, + ) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=TEST_RISCO_TO_HA, + ) + + # Test an HA state that isn't used + with pytest.raises(vol.error.MultipleInvalid): + await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={**TEST_HA_TO_RISCO, "armed_custom_bypass": "D"}, + ) + + # Test a combo that can't be selected + with pytest.raises(vol.error.MultipleInvalid): + await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={**TEST_HA_TO_RISCO, "armed_night": "A"}, + ) From 637fdf72ca1960f1a8094200a3efa93635c218be Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Fri, 28 Aug 2020 04:18:09 +0100 Subject: [PATCH 387/862] Improve volume handling for Roon (#39119) * Handle players without volume attribute. * Refactor volume and now_playing error handling, * Apply suggestions from code review Co-authored-by: Chris Talkington * Review suggestions. Co-authored-by: Chris Talkington --- homeassistant/components/roon/media_player.py | 113 ++++++++++++------ 1 file changed, 78 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index c4aa3dc8a2c..2f695b49236 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -161,8 +161,73 @@ class RoonDevice(MediaPlayerEntity): if self.state == STATE_PLAYING: self._last_position_update = utcnow() + @classmethod + def _parse_volume(cls, player_data): + """Parse volume data to determine volume levels and mute state.""" + volume = { + "level": 0, + "step": 0, + "muted": False, + } + + try: + volume_data = player_data["volume"] + volume_muted = volume_data["is_muted"] + volume_step = convert(volume_data["step"], int, 0) + + if volume_data["type"] == "db": + level = convert(volume_data["value"], float, 0.0) / 80 * 100 + 100 + else: + level = convert(volume_data["value"], float, 0.0) + + volume_level = convert(level, int, 0) / 100 + except KeyError: + # catch KeyError + pass + else: + volume["muted"] = volume_muted + volume["step"] = volume_step + volume["level"] = volume_level + + return volume + + def _parse_now_playing(self, player_data): + """Parse now playing data to determine title, artist, position, duration and artwork.""" + now_playing = { + "title": None, + "artist": None, + "album": None, + "position": 0, + "duration": 0, + "image": None, + } + now_playing_data = None + + try: + now_playing_data = player_data["now_playing"] + media_title = now_playing_data["three_line"]["line1"] + media_artist = now_playing_data["three_line"]["line2"] + media_album_name = now_playing_data["three_line"]["line3"] + media_position = convert(now_playing_data["seek_position"], int, 0) + media_duration = convert(now_playing_data["length"], int, 0) + image_id = now_playing_data.get("image_key") + except KeyError: + # catch KeyError + pass + else: + now_playing["title"] = media_title + now_playing["artist"] = media_artist + now_playing["album"] = media_album_name + now_playing["position"] = media_position + now_playing["duration"] = media_duration + if image_id: + now_playing["image"] = self._server.roonapi.get_image(image_id) + + return now_playing + def update_state(self): """Update the power state and player state.""" + new_state = "" # power state from source control (if supported) if "source_controls" in self.player_data: @@ -188,44 +253,22 @@ class RoonDevice(MediaPlayerEntity): self._unique_id = self.player_data["dev_id"] self._zone_id = self.player_data["zone_id"] self._output_id = self.player_data["output_id"] - self._name = self.player_data["display_name"] - self._is_volume_muted = self.player_data["volume"]["is_muted"] - self._volume_step = convert(self.player_data["volume"]["step"], int, 0) self._shuffle = self.player_data["settings"]["shuffle"] + self._name = self.player_data["display_name"] - if self.player_data["volume"]["type"] == "db": - volume = ( - convert(self.player_data["volume"]["value"], float, 0.0) / 80 * 100 - + 100 - ) - else: - volume = convert(self.player_data["volume"]["value"], float, 0.0) - self._volume_level = convert(volume, int, 0) / 100 + volume = RoonDevice._parse_volume(self.player_data) + self._is_volume_muted = volume["muted"] + self._volume_step = volume["step"] + self._is_volume_muted = volume["muted"] + self._volume_level = volume["level"] - try: - self._media_title = self.player_data["now_playing"]["three_line"]["line1"] - self._media_artist = self.player_data["now_playing"]["three_line"]["line2"] - self._media_album_name = self.player_data["now_playing"]["three_line"][ - "line3" - ] - self._media_position = convert( - self.player_data["now_playing"]["seek_position"], int, 0 - ) - self._media_duration = convert( - self.player_data["now_playing"]["length"], int, 0 - ) - try: - image_id = self.player_data["now_playing"]["image_key"] - self._media_image_url = self._server.roonapi.get_image(image_id) - except KeyError: - self._media_image_url = None - except KeyError: - self._media_title = None - self._media_album_name = None - self._media_artist = None - self._media_position = 0 - self._media_duration = 0 - self._media_image_url = None + now_playing = self._parse_now_playing(self.player_data) + self._media_title = now_playing["title"] + self._media_artist = now_playing["artist"] + self._media_album_name = now_playing["album"] + self._media_position = now_playing["position"] + self._media_duration = now_playing["duration"] + self._media_image_url = now_playing["image"] @property def media_position_updated_at(self): From 526c418e1e060120c16e75ab3d3d7c3cad7eb33f Mon Sep 17 00:00:00 2001 From: "Bill (William) O'Neill" Date: Thu, 27 Aug 2020 23:22:28 -0400 Subject: [PATCH 388/862] Support selecting http vs https protocols for qvrpro (#38951) * Support selecting http vs https protocols for qvrpro * Make protocol selection limited to http or https --- homeassistant/components/qvr_pro/__init__.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/qvr_pro/__init__.py b/homeassistant/components/qvr_pro/__init__.py index ed12cd49c51..d7f5c3e93cb 100644 --- a/homeassistant/components/qvr_pro/__init__.py +++ b/homeassistant/components/qvr_pro/__init__.py @@ -8,7 +8,13 @@ from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -20,6 +26,7 @@ from .const import ( ) DEFAULT_PORT = 8080 +DEFAULT_PROTOCOL = "http" SERVICE_CHANNEL_GUID = "guid" @@ -33,6 +40,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In(["http", "https"]) + ), vol.Optional(CONF_EXCLUDE_CHANNELS, default=[]): vol.All( cv.ensure_list_csv, [cv.positive_int] ), @@ -54,10 +64,11 @@ def setup(hass, config): password = conf[CONF_PASSWORD] host = conf[CONF_HOST] port = conf[CONF_PORT] + protocol = conf[CONF_PROTOCOL] excluded_channels = conf[CONF_EXCLUDE_CHANNELS] try: - qvrpro = Client(user, password, host, port=port) + qvrpro = Client(user, password, host, protocol=protocol, port=port) channel_resp = qvrpro.get_channel_list() From 0db5bb27a87b54ad6669d130806f9c5f9123ebf3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Aug 2020 23:50:28 -0500 Subject: [PATCH 389/862] Add the ability to reload trend platforms from yaml (#39341) --- homeassistant/components/trend/__init__.py | 3 ++ .../components/trend/binary_sensor.py | 12 ++++- homeassistant/components/trend/services.yaml | 2 + tests/components/trend/test_binary_sensor.py | 49 ++++++++++++++++++- tests/fixtures/trend/configuration.yaml | 5 ++ 5 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/trend/services.yaml create mode 100644 tests/fixtures/trend/configuration.yaml diff --git a/homeassistant/components/trend/__init__.py b/homeassistant/components/trend/__init__.py index 77159060263..12ce7fdfe49 100644 --- a/homeassistant/components/trend/__init__.py +++ b/homeassistant/components/trend/__init__.py @@ -1 +1,4 @@ """A sensor that monitors trends in other components.""" + +DOMAIN = "trend" +PLATFORMS = ["binary_sensor"] diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index cd78618d156..1c6c75d6ce5 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -26,8 +26,11 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.reload import setup_reload_service from homeassistant.util import utcnow +from . import DOMAIN, PLATFORMS + _LOGGER = logging.getLogger(__name__) ATTR_ATTRIBUTE = "attribute" @@ -63,6 +66,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the trend sensors.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + sensors = [] for device_id, device_config in config[CONF_SENSORS].items(): @@ -179,8 +185,10 @@ class SensorTrend(BinarySensorEntity): except (ValueError, TypeError) as ex: _LOGGER.error(ex) - async_track_state_change_event( - self.hass, [self._entity_id], trend_sensor_state_listener + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._entity_id], trend_sensor_state_listener + ) ) async def async_update(self): diff --git a/homeassistant/components/trend/services.yaml b/homeassistant/components/trend/services.yaml new file mode 100644 index 00000000000..6c4b027ef99 --- /dev/null +++ b/homeassistant/components/trend/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all trend entities. diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index 0f64afe27cd..43db97b8f80 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -1,7 +1,10 @@ """The test for the Trend sensor platform.""" from datetime import timedelta +from os import path -from homeassistant import setup +from homeassistant import config as hass_config, setup +from homeassistant.components.trend import DOMAIN +from homeassistant.const import SERVICE_RELOAD import homeassistant.util.dt as dt_util from tests.async_mock import patch @@ -370,3 +373,47 @@ class TestTrendBinarySensor: self.hass, "binary_sensor", {"binary_sensor": {"platform": "trend"}} ) assert self.hass.states.all() == [] + + +async def test_reload(hass): + """Verify we can reload trend sensors.""" + hass.states.async_set("sensor.test_state", 1234) + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "trend", + "sensors": {"test_trend_sensor": {"entity_id": "sensor.test_state"}}, + } + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("binary_sensor.test_trend_sensor") + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "trend/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("binary_sensor.test_trend_sensor") is None + assert hass.states.get("binary_sensor.second_test_trend_sensor") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/trend/configuration.yaml b/tests/fixtures/trend/configuration.yaml new file mode 100644 index 00000000000..a3826cbb9e3 --- /dev/null +++ b/tests/fixtures/trend/configuration.yaml @@ -0,0 +1,5 @@ +binary_sensor: + - platform: trend + sensors: + second_test_trend_sensor: + entity_id: sensor.test_state From f449620d38b95f2d4c88d44af473df324f1ce9f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Aug 2020 23:53:27 -0500 Subject: [PATCH 390/862] Add the ability to reload filesize platforms from yaml (#39347) --- homeassistant/components/filesize/__init__.py | 3 ++ homeassistant/components/filesize/sensor.py | 6 +++ .../components/filesize/services.yaml | 2 + tests/components/filesize/test_sensor.py | 50 ++++++++++++++++++- tests/fixtures/filesize/configuration.yaml | 4 ++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/filesize/services.yaml create mode 100644 tests/fixtures/filesize/configuration.yaml diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index c532274997c..d209a845b15 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -1 +1,4 @@ """The filesize component.""" + +DOMAIN = "filesize" +PLATFORMS = ["sensor"] diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 3d96aab04e9..63042ec6292 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -9,6 +9,9 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import DATA_MEGABYTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.reload import setup_reload_service + +from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -23,6 +26,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the file size sensor.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + sensors = [] for path in config.get(CONF_FILE_PATHS): if not hass.config.is_allowed_path(path): diff --git a/homeassistant/components/filesize/services.yaml b/homeassistant/components/filesize/services.yaml new file mode 100644 index 00000000000..9f251b50e7c --- /dev/null +++ b/homeassistant/components/filesize/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all filesize entities. diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 6d35ab0d88e..e0633d69d03 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -2,9 +2,13 @@ import os import unittest +from homeassistant import config as hass_config +from homeassistant.components.filesize import DOMAIN from homeassistant.components.filesize.sensor import CONF_FILE_PATHS -from homeassistant.setup import setup_component +from homeassistant.const import SERVICE_RELOAD +from homeassistant.setup import async_setup_component, setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant TEST_DIR = os.path.join(os.path.dirname(__file__)) @@ -47,3 +51,47 @@ class TestFileSensor(unittest.TestCase): state = self.hass.states.get("sensor.mock_file_test_filesize_txt") assert state.state == "0.0" assert state.attributes.get("bytes") == 4 + + +async def test_reload(hass, tmpdir): + """Verify we can reload filter sensors.""" + testfile = f"{tmpdir}/file" + await hass.async_add_executor_job(create_file, testfile) + with patch.object(hass.config, "is_allowed_path", return_value=True): + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "filesize", + "file_paths": [testfile], + } + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("sensor.file") + + yaml_path = os.path.join( + _get_fixtures_base_path(), + "fixtures", + "filesize/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch.object( + hass.config, "is_allowed_path", return_value=True + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.file") is None + + +def _get_fixtures_base_path(): + return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) diff --git a/tests/fixtures/filesize/configuration.yaml b/tests/fixtures/filesize/configuration.yaml new file mode 100644 index 00000000000..ea73be72b80 --- /dev/null +++ b/tests/fixtures/filesize/configuration.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: filesize + file_paths: + - "/dev/null" From 77490287e95323c75d777a87703ab250c1d53386 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Aug 2020 23:59:49 -0500 Subject: [PATCH 391/862] Add the ability to reload generic_thermostat platforms from yaml (#39291) --- .../components/generic_thermostat/__init__.py | 3 ++ .../components/generic_thermostat/climate.py | 24 +++++++--- .../generic_thermostat/services.yaml | 2 + .../generic_thermostat/test_climate.py | 44 +++++++++++++++++++ .../generic_thermostat/configuration.yaml | 5 +++ 5 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/generic_thermostat/configuration.yaml diff --git a/homeassistant/components/generic_thermostat/__init__.py b/homeassistant/components/generic_thermostat/__init__.py index d0bc392e4f4..69acb5bb1a5 100644 --- a/homeassistant/components/generic_thermostat/__init__.py +++ b/homeassistant/components/generic_thermostat/__init__.py @@ -1 +1,4 @@ """The generic_thermostat component.""" + +DOMAIN = "generic_thermostat" +PLATFORMS = ["climate"] diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 31231d1ffb2..4072c43bc27 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -40,8 +40,11 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_interval, ) +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity +from . import DOMAIN, PLATFORMS + _LOGGER = logging.getLogger(__name__) DEFAULT_TOLERANCE = 0.3 @@ -88,6 +91,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the generic thermostat platform.""" + + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + name = config.get(CONF_NAME) heater_entity_id = config.get(CONF_HEATER) sensor_entity_id = config.get(CONF_SENSOR) @@ -182,16 +188,22 @@ class GenericThermostat(ClimateEntity, RestoreEntity): await super().async_added_to_hass() # Add listener - async_track_state_change_event( - self.hass, [self.sensor_entity_id], self._async_sensor_changed + self.async_on_remove( + async_track_state_change_event( + self.hass, [self.sensor_entity_id], self._async_sensor_changed + ) ) - async_track_state_change_event( - self.hass, [self.heater_entity_id], self._async_switch_changed + self.async_on_remove( + async_track_state_change_event( + self.hass, [self.heater_entity_id], self._async_switch_changed + ) ) if self._keep_alive: - async_track_time_interval( - self.hass, self._async_control_heating, self._keep_alive + self.async_on_remove( + async_track_time_interval( + self.hass, self._async_control_heating, self._keep_alive + ) ) @callback diff --git a/homeassistant/components/generic_thermostat/services.yaml b/homeassistant/components/generic_thermostat/services.yaml index e69de29bb2d..fedcd268253 100644 --- a/homeassistant/components/generic_thermostat/services.yaml +++ b/homeassistant/components/generic_thermostat/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all generic_thermostat entities. diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 313ff43ca6a..71842cf3b6d 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1,10 +1,12 @@ """The tests for the generic_thermostat.""" import datetime +from os import path import pytest import pytz import voluptuous as vol +from homeassistant import config as hass_config from homeassistant.components import input_boolean, switch from homeassistant.components.climate.const import ( ATTR_PRESET_MODE, @@ -15,8 +17,12 @@ from homeassistant.components.climate.const import ( PRESET_AWAY, PRESET_NONE, ) +from homeassistant.components.generic_thermostat import ( + DOMAIN as GENERIC_THERMOSTAT_DOMAIN, +) from homeassistant.const import ( ATTR_TEMPERATURE, + SERVICE_RELOAD, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -1246,3 +1252,41 @@ def _mock_restore_cache(hass, temperature=20, hvac_mode=HVAC_MODE_OFF): ), ), ) + + +async def test_reload(hass): + """Test we can reload.""" + + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "heater": "switch.any", + "target_sensor": "sensor.any", + } + }, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + assert hass.states.get("climate.test") is not None + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "generic_thermostat/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + GENERIC_THERMOSTAT_DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + assert hass.states.get("climate.test") is None + assert hass.states.get("climate.reload") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/generic_thermostat/configuration.yaml b/tests/fixtures/generic_thermostat/configuration.yaml new file mode 100644 index 00000000000..48b5ee2ed7b --- /dev/null +++ b/tests/fixtures/generic_thermostat/configuration.yaml @@ -0,0 +1,5 @@ +climate: + - platform: generic_thermostat + name: reload + heater: switch.any + target_sensor: sensor.any From b5c2c9ec9b5faea35ce334576351d437fd83146b Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 28 Aug 2020 07:08:37 +0200 Subject: [PATCH 392/862] Add device attribute for homematicip_cloud rotary handle (#39144) --- .../homematicip_cloud/binary_sensor.py | 26 ++++++++++++++++--- .../homematicip_cloud/test_binary_sensor.py | 8 ++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index aae7f881be0..0867fa7002a 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -87,11 +87,10 @@ async def async_setup_entry( entities.append(HomematicipAccelerationSensor(hap, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): entities.append(HomematicipContactInterface(hap, device)) - if isinstance( - device, - (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), - ): + if isinstance(device, (AsyncShutterContact, AsyncShutterContactMagnetic),): entities.append(HomematicipShutterContact(hap, device)) + if isinstance(device, AsyncRotaryHandleSensor): + entities.append(HomematicipShutterContact(hap, device, True)) if isinstance( device, ( @@ -176,6 +175,13 @@ class HomematicipContactInterface(HomematicipGenericEntity, BinarySensorEntity): class HomematicipShutterContact(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP shutter contact.""" + def __init__( + self, hap: HomematicipHAP, device, has_additional_state: bool = False + ) -> None: + """Initialize the shutter contact.""" + super().__init__(hap, device) + self.has_additional_state = has_additional_state + @property def device_class(self) -> str: """Return the class of this sensor.""" @@ -188,6 +194,18 @@ class HomematicipShutterContact(HomematicipGenericEntity, BinarySensorEntity): return None return self._device.windowState != WindowState.CLOSED + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the state attributes of the Shutter Contact.""" + state_attr = super().device_state_attributes + + if self.has_additional_state: + window_state = getattr(self._device, "windowState", None) + if window_state and window_state != WindowState.CLOSED: + state_attr[ATTR_WINDOW_STATE] = window_state + + return state_attr + class HomematicipMotionDetector(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP motion detector.""" diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 6be0036a737..06b19ae0805 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -112,11 +112,19 @@ async def test_hmip_shutter_contact(hass, default_mock_hap_factory): ) assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_WINDOW_STATE] == WindowState.TILTED + + await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_WINDOW_STATE] == WindowState.OPEN + await async_manipulate_test_data( hass, hmip_device, "windowState", WindowState.CLOSED ) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_WINDOW_STATE) await async_manipulate_test_data(hass, hmip_device, "windowState", None) ha_state = hass.states.get(entity_id) From f914625b8ad518bbee4c721f951854396ea0b1a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 00:22:43 -0500 Subject: [PATCH 393/862] Add the ability to reload history_stats platforms from yaml (#39337) * Add the ability to reload history_stats platforms from yaml * Increase coverage and cleanup * Fix coverage * services.yaml --- .../components/history_stats/__init__.py | 3 + .../components/history_stats/sensor.py | 24 +++++-- .../components/history_stats/services.yaml | 2 + tests/components/history_stats/test_sensor.py | 62 ++++++++++++++++++- .../fixtures/history_stats/configuration.yaml | 7 +++ 5 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/history_stats/services.yaml create mode 100644 tests/fixtures/history_stats/configuration.yaml diff --git a/homeassistant/components/history_stats/__init__.py b/homeassistant/components/history_stats/__init__.py index 3c5385be6ad..dcdca70b71c 100644 --- a/homeassistant/components/history_stats/__init__.py +++ b/homeassistant/components/history_stats/__init__.py @@ -1 +1,4 @@ """The history_stats component.""" + +DOMAIN = "history_stats" +PLATFORMS = ["sensor"] diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 2c77261d344..e4ff1ab5b28 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -16,16 +16,18 @@ from homeassistant.const import ( TIME_HOURS, UNIT_PERCENTAGE, ) -from homeassistant.core import callback +from homeassistant.core import CoreState, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.reload import setup_reload_service import homeassistant.util.dt as dt_util +from . import DOMAIN, PLATFORMS + _LOGGER = logging.getLogger(__name__) -DOMAIN = "history_stats" CONF_START = "start" CONF_END = "end" CONF_DURATION = "duration" @@ -75,6 +77,9 @@ PLATFORM_SCHEMA = vol.All( # noinspection PyUnusedLocal def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the History Stats sensor.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + entity_id = config.get(CONF_ENTITY_ID) entity_state = config.get(CONF_STATE) start = config.get(CONF_START) @@ -118,6 +123,9 @@ class HistoryStatsSensor(Entity): self.value = None self.count = None + async def async_added_to_hass(self): + """Create listeners when the entity is added.""" + @callback def start_refresh(*args): """Register state tracking.""" @@ -128,10 +136,18 @@ class HistoryStatsSensor(Entity): self.async_schedule_update_ha_state(True) force_refresh() - async_track_state_change_event(self.hass, [self._entity_id], force_refresh) + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._entity_id], force_refresh + ) + ) + + if self.hass.state == CoreState.running: + start_refresh() + return # Delay first refresh to keep startup fast - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_refresh) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_refresh) @property def name(self): diff --git a/homeassistant/components/history_stats/services.yaml b/homeassistant/components/history_stats/services.yaml new file mode 100644 index 00000000000..38758a35df0 --- /dev/null +++ b/homeassistant/components/history_stats/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all history_stats entities. diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 25cca7615a7..bab6eae4564 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1,16 +1,19 @@ """The test for the History Statistics sensor platform.""" # pylint: disable=protected-access from datetime import datetime, timedelta +from os import path import unittest import pytest import pytz +from homeassistant import config as hass_config +from homeassistant.components.history_stats import DOMAIN from homeassistant.components.history_stats.sensor import HistoryStatsSensor -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN import homeassistant.core as ha from homeassistant.helpers.template import Template -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util from tests.async_mock import patch @@ -255,3 +258,58 @@ class TestHistoryStatsSensor(unittest.TestCase): """Initialize the recorder.""" init_recorder_component(self.hass) self.hass.start() + + +async def test_reload(hass): + """Verify we can reload history_stats sensors.""" + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db + + hass.state = ha.CoreState.not_running + hass.states.async_set("binary_sensor.test_id", "on") + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "test", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "duration": "01:00", + }, + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("sensor.test") + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "history_stats/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("sensor.test") is None + assert hass.states.get("sensor.second_test") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/history_stats/configuration.yaml b/tests/fixtures/history_stats/configuration.yaml new file mode 100644 index 00000000000..15704996998 --- /dev/null +++ b/tests/fixtures/history_stats/configuration.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: history_stats + entity_id: binary_sensor.test_id + name: second_test + state: "on" + start: "{{ as_timestamp(now()) - 3600 }}" + end: "{{ now() }}" From 98db0a2d2e27c69e8a766a739088f98976727a75 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 28 Aug 2020 00:24:11 -0500 Subject: [PATCH 394/862] fix black on generic_thermostat tests (#39350) --- tests/components/generic_thermostat/test_climate.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 71842cf3b6d..eaf7c8e5651 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1275,11 +1275,16 @@ async def test_reload(hass): assert hass.states.get("climate.test") is not None yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "generic_thermostat/configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "generic_thermostat/configuration.yaml", ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - GENERIC_THERMOSTAT_DOMAIN, SERVICE_RELOAD, {}, blocking=True, + GENERIC_THERMOSTAT_DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() From ab90ea78846c91c16432137e8e056d416f075b85 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 28 Aug 2020 00:47:27 -0500 Subject: [PATCH 395/862] Fix black on homematicip_cloud binary_sensor (#39351) --- homeassistant/components/homematicip_cloud/binary_sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 0867fa7002a..15ba9049dba 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -87,7 +87,10 @@ async def async_setup_entry( entities.append(HomematicipAccelerationSensor(hap, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): entities.append(HomematicipContactInterface(hap, device)) - if isinstance(device, (AsyncShutterContact, AsyncShutterContactMagnetic),): + if isinstance( + device, + (AsyncShutterContact, AsyncShutterContactMagnetic), + ): entities.append(HomematicipShutterContact(hap, device)) if isinstance(device, AsyncRotaryHandleSensor): entities.append(HomematicipShutterContact(hap, device, True)) From d768fd4de9579aa3bf86f5951b20865ee13925e8 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 28 Aug 2020 13:06:25 +0200 Subject: [PATCH 396/862] Bump arcam fmj with no install requires on asyncio (#39353) --- homeassistant/components/arcam_fmj/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 053c0372d25..fb66687611c 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -3,7 +3,7 @@ "name": "Arcam FMJ Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", - "requirements": ["arcam-fmj==0.5.1"], + "requirements": ["arcam-fmj==0.5.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/requirements_all.txt b/requirements_all.txt index af78e8aeaf6..96aecbde2ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -272,7 +272,7 @@ aprslib==0.6.46 aqualogic==1.0 # homeassistant.components.arcam_fmj -arcam-fmj==0.5.1 +arcam-fmj==0.5.2 # homeassistant.components.arris_tg2492lg arris-tg2492lg==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4356d0bc2a9..c402aabd39e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ apprise==0.8.7 aprslib==0.6.46 # homeassistant.components.arcam_fmj -arcam-fmj==0.5.1 +arcam-fmj==0.5.2 # homeassistant.components.dlna_dmr # homeassistant.components.upnp From b4bac0f7a0a881ece3b3bd06dbe8531a67f8c923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 28 Aug 2020 14:50:32 +0300 Subject: [PATCH 397/862] Exception chaining and wrapping improvements (#39320) * Remove unnecessary exception re-wraps * Preserve exception chains on re-raise We slap "from cause" to almost all possible cases here. In some cases it could conceivably be better to do "from None" if we really want to hide the cause. However those should be in the minority, and "from cause" should be an improvement over the corresponding raise without a "from" in all cases anyway. The only case where we raise from None here is in plex, where the exception for an original invalid SSL cert is not the root cause for failure to validate a newly fetched one. Follow local convention on exception variable names if there is a consistent one, otherwise `err` to match with majority of codebase. * Fix mistaken re-wrap in homematicip_cloud/hap.py Missed the difference between HmipConnectionError and HmipcConnectionError. * Do not hide original error on plex new cert validation error Original is not the cause for the new one, but showing old in the traceback is useful nevertheless. --- homeassistant/auth/mfa_modules/__init__.py | 4 ++- homeassistant/auth/providers/__init__.py | 4 ++- homeassistant/auth/providers/command_line.py | 2 +- homeassistant/components/abode/__init__.py | 2 +- .../components/accuweather/__init__.py | 2 +- .../components/agent_dvr/__init__.py | 4 +-- homeassistant/components/airly/__init__.py | 2 +- .../components/airvisual/__init__.py | 4 +-- homeassistant/components/alexa/handlers.py | 6 ++-- homeassistant/components/almond/__init__.py | 2 +- .../components/ambient_station/__init__.py | 2 +- homeassistant/components/api/__init__.py | 4 +-- .../components/asterisk_mbox/mailbox.py | 2 +- homeassistant/components/atag/__init__.py | 2 +- homeassistant/components/aten_pe/switch.py | 2 +- homeassistant/components/august/__init__.py | 6 ++-- homeassistant/components/august/gateway.py | 2 +- homeassistant/components/auth/indieauth.py | 4 +-- homeassistant/components/awair/__init__.py | 4 +-- homeassistant/components/axis/device.py | 16 +++++----- homeassistant/components/blink/config_flow.py | 4 +-- homeassistant/components/bmp280/sensor.py | 2 +- homeassistant/components/bond/config_flow.py | 12 +++---- homeassistant/components/broadlink/device.py | 4 +-- homeassistant/components/broadlink/remote.py | 8 ++--- homeassistant/components/broadlink/updater.py | 2 +- homeassistant/components/brother/__init__.py | 2 +- homeassistant/components/camera/__init__.py | 4 +-- .../components/cert_expiry/__init__.py | 2 +- .../components/cert_expiry/helper.py | 20 +++++++----- homeassistant/components/citybikes/sensor.py | 8 ++--- homeassistant/components/control4/__init__.py | 2 +- homeassistant/components/control4/light.py | 4 +-- homeassistant/components/daikin/__init__.py | 8 ++--- homeassistant/components/deconz/gateway.py | 12 +++---- homeassistant/components/deluge/sensor.py | 4 +-- homeassistant/components/deluge/switch.py | 4 +-- .../components/device_automation/__init__.py | 10 +++--- .../devolo_home_control/__init__.py | 4 +-- homeassistant/components/dexcom/__init__.py | 6 ++-- homeassistant/components/directv/__init__.py | 4 +-- .../components/dlna_dmr/media_player.py | 4 +-- homeassistant/components/doorbird/__init__.py | 4 +-- .../components/doorbird/config_flow.py | 8 ++--- homeassistant/components/ebox/sensor.py | 2 +- homeassistant/components/ebusd/__init__.py | 2 +- homeassistant/components/ecobee/util.py | 10 +++--- homeassistant/components/ecobee/weather.py | 4 +-- homeassistant/components/everlights/light.py | 4 +-- .../components/flick_electric/config_flow.py | 8 ++--- homeassistant/components/flo/__init__.py | 4 +-- homeassistant/components/flo/config_flow.py | 2 +- homeassistant/components/flo/device.py | 2 +- homeassistant/components/flume/__init__.py | 4 +-- homeassistant/components/flume/config_flow.py | 8 ++--- .../components/flunearyou/__init__.py | 4 +-- homeassistant/components/foobot/sensor.py | 4 +-- .../components/garmin_connect/__init__.py | 2 +- homeassistant/components/gios/__init__.py | 2 +- homeassistant/components/glances/__init__.py | 4 +-- .../components/glances/config_flow.py | 4 +-- homeassistant/components/gogogate2/common.py | 4 ++- .../components/griddy/config_flow.py | 4 +-- homeassistant/components/guardian/util.py | 2 +- homeassistant/components/harmony/__init__.py | 4 +-- homeassistant/components/hassio/auth.py | 4 +-- homeassistant/components/heos/__init__.py | 4 +-- .../components/hisense_aehw4a1/__init__.py | 4 +-- .../components/hlk_sw16/config_flow.py | 6 ++-- .../homeassistant/triggers/time_pattern.py | 4 +-- .../components/homematicip_cloud/hap.py | 8 ++--- .../components/horizon/media_player.py | 2 +- homeassistant/components/hp_ilo/sensor.py | 2 +- homeassistant/components/http/forwarded.py | 4 +-- homeassistant/components/http/view.py | 14 ++++---- homeassistant/components/hue/bridge.py | 16 +++++----- homeassistant/components/hue/light.py | 6 ++-- homeassistant/components/hue/sensor_base.py | 6 ++-- .../hunterdouglas_powerview/__init__.py | 4 +-- .../hunterdouglas_powerview/config_flow.py | 4 +-- homeassistant/components/iammeter/sensor.py | 8 ++--- .../components/iaqualink/__init__.py | 2 +- homeassistant/components/icloud/account.py | 7 ++-- homeassistant/components/image/__init__.py | 8 ++--- homeassistant/components/influxdb/__init__.py | 24 +++++++------- homeassistant/components/influxdb/sensor.py | 2 +- homeassistant/components/insteon/schemas.py | 8 ++--- .../components/intesishome/climate.py | 6 ++-- homeassistant/components/ipp/__init__.py | 2 +- .../islamic_prayer_times/__init__.py | 4 +-- .../components/isy994/config_flow.py | 2 +- homeassistant/components/juicenet/__init__.py | 2 +- .../components/juicenet/config_flow.py | 4 +-- .../components/konnected/config_flow.py | 4 +-- homeassistant/components/konnected/panel.py | 2 +- .../components/luftdaten/__init__.py | 4 +-- .../components/media_extractor/__init__.py | 8 ++--- homeassistant/components/met/__init__.py | 2 +- homeassistant/components/mikrotik/hub.py | 10 +++--- .../components/mobile_app/webhook.py | 2 +- homeassistant/components/modbus/sensor.py | 4 +-- .../components/monoprice/__init__.py | 4 +-- .../components/monoprice/config_flow.py | 4 +-- homeassistant/components/mqtt/util.py | 4 +-- homeassistant/components/myq/__init__.py | 4 +-- homeassistant/components/myq/config_flow.py | 8 ++--- homeassistant/components/mysensors/gateway.py | 4 +-- homeassistant/components/mystrom/light.py | 4 +-- homeassistant/components/mystrom/switch.py | 4 +-- homeassistant/components/neato/__init__.py | 6 ++-- .../nederlandse_spoorwegen/sensor.py | 2 +- homeassistant/components/nexia/__init__.py | 4 +-- homeassistant/components/nexia/config_flow.py | 6 ++-- .../components/nightscout/config_flow.py | 4 +-- .../components/niko_home_control/light.py | 2 +- homeassistant/components/notion/__init__.py | 2 +- homeassistant/components/nuheat/__init__.py | 6 ++-- .../components/nuheat/config_flow.py | 16 +++++----- homeassistant/components/numato/__init__.py | 8 ++--- homeassistant/components/nws/config_flow.py | 2 +- .../components/nx584/alarm_control_panel.py | 2 +- homeassistant/components/openuv/__init__.py | 2 +- homeassistant/components/pencom/switch.py | 2 +- homeassistant/components/pi_hole/__init__.py | 4 +-- homeassistant/components/plex/__init__.py | 2 +- homeassistant/components/plugwise/__init__.py | 12 +++---- .../components/plugwise/config_flow.py | 8 ++--- .../components/plum_lightpad/__init__.py | 2 +- .../components/poolsense/__init__.py | 2 +- .../components/powerwall/__init__.py | 8 ++--- .../components/powerwall/config_flow.py | 6 ++-- homeassistant/components/proxy/camera.py | 8 ++--- .../components/qbittorrent/sensor.py | 4 +-- homeassistant/components/rachio/__init__.py | 2 +- .../components/rachio/config_flow.py | 2 +- .../components/rainmachine/__init__.py | 2 +- .../components/raspihats/__init__.py | 6 ++-- homeassistant/components/rfxtrx/__init__.py | 6 ++-- homeassistant/components/ring/config_flow.py | 8 ++--- homeassistant/components/roku/__init__.py | 2 +- homeassistant/components/roomba/__init__.py | 12 +++---- homeassistant/components/rtorrent/sensor.py | 4 +-- homeassistant/components/schluter/climate.py | 2 +- homeassistant/components/sense/__init__.py | 8 ++--- homeassistant/components/sensibo/climate.py | 4 +-- homeassistant/components/shelly/__init__.py | 8 ++--- .../components/simplisafe/__init__.py | 2 +- homeassistant/components/sisyphus/light.py | 4 +-- .../components/sisyphus/media_player.py | 4 +-- .../components/smart_meter_texas/__init__.py | 6 ++-- .../smart_meter_texas/config_flow.py | 6 ++-- homeassistant/components/smarthab/__init__.py | 4 +-- .../components/smartthings/__init__.py | 4 +-- homeassistant/components/sms/config_flow.py | 4 +-- homeassistant/components/solax/sensor.py | 4 +-- homeassistant/components/sonarr/__init__.py | 4 +-- .../components/songpal/media_player.py | 2 +- .../components/speedtestdotnet/__init__.py | 8 ++--- homeassistant/components/spotify/__init__.py | 4 +-- homeassistant/components/stream/__init__.py | 4 +-- homeassistant/components/tado/__init__.py | 2 +- homeassistant/components/tado/config_flow.py | 12 +++---- .../components/tankerkoenig/sensor.py | 4 +-- homeassistant/components/tesla/__init__.py | 2 +- homeassistant/components/tesla/config_flow.py | 4 +-- homeassistant/components/tibber/__init__.py | 4 +-- homeassistant/components/tibber/sensor.py | 4 +-- homeassistant/components/tile/__init__.py | 2 +- homeassistant/components/toon/coordinator.py | 2 +- homeassistant/components/tradfri/__init__.py | 4 +-- .../components/tradfri/config_flow.py | 12 +++---- .../components/transmission/__init__.py | 10 +++--- homeassistant/components/tts/__init__.py | 8 ++--- homeassistant/components/tuya/__init__.py | 4 +-- homeassistant/components/unifi/controller.py | 16 +++++----- homeassistant/components/updater/__init__.py | 8 +++-- homeassistant/components/upnp/__init__.py | 4 +-- homeassistant/components/uvc/camera.py | 4 +-- homeassistant/components/velbus/__init__.py | 2 +- homeassistant/components/waqi/sensor.py | 7 ++-- .../components/websocket_api/auth.py | 2 +- .../components/websocket_api/http.py | 8 ++--- homeassistant/components/wled/__init__.py | 2 +- homeassistant/components/wolflink/__init__.py | 14 ++++---- .../components/workday/binary_sensor.py | 4 +-- .../components/xiaomi_miio/air_quality.py | 4 +-- homeassistant/components/xiaomi_miio/fan.py | 4 +-- homeassistant/components/xiaomi_miio/light.py | 4 +-- .../components/xiaomi_miio/remote.py | 2 +- .../components/xiaomi_miio/sensor.py | 4 +-- .../components/xiaomi_miio/switch.py | 4 +-- homeassistant/components/yi/camera.py | 2 +- homeassistant/components/zha/api.py | 8 ++--- .../components/zha/device_trigger.py | 4 +-- homeassistant/config_entries.py | 4 +-- homeassistant/helpers/config_validation.py | 32 +++++++++---------- homeassistant/helpers/frame.py | 6 ++-- homeassistant/helpers/network.py | 4 +-- homeassistant/helpers/script.py | 12 +++---- homeassistant/helpers/template.py | 4 +-- homeassistant/scripts/check_config.py | 2 +- homeassistant/util/json.py | 10 +++--- homeassistant/util/ruamel_yaml.py | 12 +++---- homeassistant/util/yaml/loader.py | 14 ++++---- 204 files changed, 550 insertions(+), 518 deletions(-) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index c2ec2260cf2..6e4b189bf74 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -150,7 +150,9 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul module = importlib.import_module(module_path) except ImportError as err: _LOGGER.error("Unable to load mfa module %s: %s", module_name, err) - raise HomeAssistantError(f"Unable to load mfa module {module_name}: {err}") + raise HomeAssistantError( + f"Unable to load mfa module {module_name}: {err}" + ) from err if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 35208bd847c..b60fa8eff9c 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -146,7 +146,9 @@ async def load_auth_provider_module( module = importlib.import_module(f"homeassistant.auth.providers.{provider}") except ImportError as err: _LOGGER.error("Unable to load auth provider %s: %s", provider, err) - raise HomeAssistantError(f"Unable to load auth provider {provider}: {err}") + raise HomeAssistantError( + f"Unable to load auth provider {provider}: {err}" + ) from err if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 961e1014c5e..d194d8119d1 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -71,7 +71,7 @@ class CommandLineAuthProvider(AuthProvider): except OSError as err: # happens when command doesn't exist or permission is denied _LOGGER.error("Error while authenticating %r: %s", username, err) - raise InvalidAuthError + raise InvalidAuthError from err if process.returncode != 0: _LOGGER.error( diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 92665bc1890..2ac52c87131 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -120,7 +120,7 @@ async def async_setup_entry(hass, config_entry): except (AbodeException, ConnectTimeout, HTTPError) as ex: LOGGER.error("Unable to connect to Abode: %s", str(ex)) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex for platform in ABODE_PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 1e1a434a036..c8ae14678d5 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -127,6 +127,6 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator): InvalidApiKeyError, RequestsExceededError, ) as error: - raise UpdateFailed(error) + raise UpdateFailed(error) from error _LOGGER.debug("Requests remaining: %s", self.accuweather.requests_remaining) return {**current, **{ATTR_FORECAST: forecast}} diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index cc72b1e33ae..878a100684f 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -33,9 +33,9 @@ async def async_setup_entry(hass, config_entry): agent_client = Agent(server_origin, async_get_clientsession(hass)) try: await agent_client.update() - except AgentError: + except AgentError as err: await agent_client.close() - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err if not agent_client.is_available: raise ConfigEntryNotReady diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 85071925357..de09d767b1f 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -125,7 +125,7 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): try: await measurements.update() except (AirlyError, ClientConnectorError) as error: - raise UpdateFailed(error) + raise UpdateFailed(error) from error values = measurements.current["values"] index = measurements.current["indexes"][0] diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 3449d5e865f..4e748766425 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -227,7 +227,7 @@ async def async_setup_entry(hass, config_entry): try: return await api_coro except AirVisualError as err: - raise UpdateFailed(f"Error while retrieving data: {err}") + raise UpdateFailed(f"Error while retrieving data: {err}") from err coordinator = DataUpdateCoordinator( hass, @@ -263,7 +263,7 @@ async def async_setup_entry(hass, config_entry): include_trends=False, ) except NodeProError as err: - raise UpdateFailed(f"Error while retrieving data: {err}") + raise UpdateFailed(f"Error while retrieving data: {err}") from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 7737016d573..6eeb3235a64 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1542,8 +1542,10 @@ async def async_api_initialize_camera_stream(hass, config, directive, context): require_ssl=True, require_standard_port=True, ) - except network.NoURLAvailableError: - raise AlexaInvalidValueError("Failed to find suitable URL to serve to Alexa") + except network.NoURLAvailableError as err: + raise AlexaInvalidValueError( + "Failed to find suitable URL to serve to Alexa" + ) from err payload = { "cameraStreams": [ diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 2117f809b04..b9f75ff8c6b 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -208,7 +208,7 @@ async def _configure_almond_for_ha( msg = err _LOGGER.warning("Unable to configure Almond: %s", msg) await hass.auth.async_remove_refresh_token(refresh_token) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err # Clear all other refresh tokens for token in list(user.refresh_tokens.values()): diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 89b6236d392..b3f4aeec3bd 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -300,7 +300,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient except WebsocketError as err: _LOGGER.error("Config entry failed: %s", err) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, ambient.client.websocket.disconnect() diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 001ce5d2a4e..c1f692a76ad 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -375,8 +375,8 @@ class APIDomainServicesView(HomeAssistantView): await hass.services.async_call( domain, service, data, True, self.context(request) ) - except (vol.Invalid, ServiceNotFound): - raise HTTPBadRequest() + except (vol.Invalid, ServiceNotFound) as ex: + raise HTTPBadRequest() from ex return self.json(changed_states) diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py index b3863eeb13f..62d817df9a3 100644 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -60,7 +60,7 @@ class AsteriskMailbox(Mailbox): partial(client.mp3, msgid, sync=True) ) except ServerError as err: - raise StreamError(err) + raise StreamError(err) from err async def async_get_messages(self): """Return a list of the current messages.""" diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 237a82f207a..e5d06c08756 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -66,7 +66,7 @@ class AtagDataUpdateCoordinator(DataUpdateCoordinator): if not await self.atag.update(): raise UpdateFailed("No data received") except AtagException as error: - raise UpdateFailed(error) + raise UpdateFailed(error) from error return self.atag.report diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index e5970fc4d3b..1bf54085064 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -55,7 +55,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= outlets = dev.outlets() except AtenPEError as exc: _LOGGER.error("Failed to initialize %s:%s: %s", node, serv, str(exc)) - raise PlatformNotReady + raise PlatformNotReady from exc switches = [] async for outlet in outlets: diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 9e0222dc81d..e0d7749dcbb 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -171,8 +171,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await august_gateway.async_setup(entry.data) return await async_setup_august(hass, entry, august_gateway) - except asyncio.TimeoutError: - raise ConfigEntryNotReady + except asyncio.TimeoutError as err: + raise ConfigEntryNotReady from err async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -339,7 +339,7 @@ class AugustData(AugustSubscriberMixin): device_name = self._get_device_name(device_id) if device_name is None: device_name = f"DeviceID: {device_id}" - raise HomeAssistantError(f"{device_name}: {err}") + raise HomeAssistantError(f"{device_name}: {err}") from err return ret diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index 272c50ac02a..6918907611f 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -102,7 +102,7 @@ class AugustGateway: self._authentication = await self.authenticator.async_authenticate() except ClientError as ex: _LOGGER.error("Unable to connect to August service: %s", str(ex)) - raise CannotConnect + raise CannotConnect from ex if self._authentication.state == AuthenticationState.BAD_PASSWORD: raise InvalidAuth diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 0d942bd358d..e823659f62b 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -170,8 +170,8 @@ def _parse_client_id(client_id): try: # parts raises ValueError when port cannot be parsed as int parts.port - except ValueError: - raise ValueError("Client ID contains invalid port") + except ValueError as ex: + raise ValueError("Client ID contains invalid port") from ex # Additionally, hostnames # MUST be domain names or a loopback interface and diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index 2ae436bebba..56af5d2b662 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -102,9 +102,9 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): ) ) - raise UpdateFailed(err) + raise UpdateFailed(err) from err except Exception as err: - raise UpdateFailed(err) + raise UpdateFailed(err) from err async def _fetch_air_data(self, device): """Fetch latest air quality data.""" diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 18845ce12a3..a22fea23e79 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -186,8 +186,8 @@ class AxisNetworkDevice: password=self.config_entry.data[CONF_PASSWORD], ) - except CannotConnect: - raise ConfigEntryNotReady + except CannotConnect as err: + raise ConfigEntryNotReady from err except Exception: # pylint: disable=broad-except LOGGER.error("Unknown error connecting with Axis device on %s", self.host) @@ -271,14 +271,14 @@ async def get_device(hass, host, port, username, password): return device - except axis.Unauthorized: + except axis.Unauthorized as err: LOGGER.warning("Connected to device at %s but not registered.", host) - raise AuthenticationRequired + raise AuthenticationRequired from err - except (asyncio.TimeoutError, axis.RequestError): + except (asyncio.TimeoutError, axis.RequestError) as err: LOGGER.error("Error connecting to the Axis device at %s", host) - raise CannotConnect + raise CannotConnect from err - except axis.AxisException: + except axis.AxisException as err: LOGGER.exception("Unknown Axis communication error occurred") - raise AuthenticationRequired + raise AuthenticationRequired from err diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index 63c822cfd1f..d244c316483 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -26,8 +26,8 @@ def validate_input(hass: core.HomeAssistant, auth): """Validate the user input allows us to connect.""" try: auth.startup() - except (LoginError, TokenRefreshFailed): - raise InvalidAuth + except (LoginError, TokenRefreshFailed) as err: + raise InvalidAuth from err if auth.check_key_required(): raise Require2FA diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py index 70efbce7d85..3c34408cb62 100644 --- a/homeassistant/components/bmp280/sensor.py +++ b/homeassistant/components/bmp280/sensor.py @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "%s. Hint: Check wiring and make sure that the SDO pin is tied to either ground (0x76) or VCC (0x77)", error.args[0], ) - raise PlatformNotReady() + raise PlatformNotReady() from error _LOGGER.error(error) return # use custom name if there's any diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index ca8965d4d43..49ca559685c 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -28,15 +28,15 @@ async def _validate_input(data: Dict[str, Any]) -> str: version = await bond.version() # call to non-version API is needed to validate authentication await bond.devices() - except ClientConnectionError: - raise InputValidationError("cannot_connect") + except ClientConnectionError as error: + raise InputValidationError("cannot_connect") from error except ClientResponseError as error: if error.status == 401: - raise InputValidationError("invalid_auth") - raise InputValidationError("unknown") - except Exception: + raise InputValidationError("invalid_auth") from error + raise InputValidationError("unknown") from error + except Exception as error: _LOGGER.exception("Unexpected exception") - raise InputValidationError("unknown") + raise InputValidationError("unknown") from error # Return unique ID from the hub to be stored in the config entry. bond_id = version.get("bondid") diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index c8751182cb9..d05fdfd4df6 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -82,8 +82,8 @@ class BroadlinkDevice: await self._async_handle_auth_error() return False - except (DeviceOfflineError, OSError): - raise ConfigEntryNotReady + except (DeviceOfflineError, OSError) as err: + raise ConfigEntryNotReady from err except BroadlinkException as err: _LOGGER.error( diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 10c45fec262..7c1ae7349da 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -175,8 +175,8 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): try: code = self._codes[device][command] - except KeyError: - raise KeyError("Command not found") + except KeyError as err: + raise KeyError("Command not found") from err # For toggle commands, alternate between codes in a list. if isinstance(code, list): @@ -187,8 +187,8 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): try: return data_packet(code), is_toggle_cmd - except ValueError: - raise ValueError("Invalid code") + except ValueError as err: + raise ValueError("Invalid code") from err @callback def get_flags(self): diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index a81cde49737..bd124d1e1ac 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -63,7 +63,7 @@ class BroadlinkUpdateManager(ABC): _LOGGER.warning( "Disconnected from the device at %s", self.device.api.host[0] ) - raise UpdateFailed(err) + raise UpdateFailed(err) from err else: if self.available is False: diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 4c69282a996..d4cd5d4a2b5 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -82,5 +82,5 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator): try: await self.brother.async_update() except (ConnectionError, SnmpError, UnsupportedModel) as error: - raise UpdateFailed(error) + raise UpdateFailed(error) from error return self.brother.data diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index bdc94b19eb1..3de47db80d1 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -539,8 +539,8 @@ class CameraMjpegStream(CameraView): if interval < MIN_STREAM_INTERVAL: raise ValueError(f"Stream interval must be be > {MIN_STREAM_INTERVAL}") return await camera.handle_async_still_stream(request, interval) - except ValueError: - raise web.HTTPBadRequest() + except ValueError as err: + raise web.HTTPBadRequest() from err @websocket_api.async_response diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 84629353bce..d01e38a2e2c 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -76,7 +76,7 @@ class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime]): try: timestamp = await get_cert_expiry_timestamp(self.hass, self.host, self.port) except TemporaryFailure as err: - raise UpdateFailed(err.args[0]) + raise UpdateFailed(err.args[0]) from err except ValidationFailure as err: self.cert_error = err self.is_cert_valid = False diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py index f4caee8abf2..6c49e9e26b9 100644 --- a/homeassistant/components/cert_expiry/helper.py +++ b/homeassistant/components/cert_expiry/helper.py @@ -28,16 +28,20 @@ async def get_cert_expiry_timestamp(hass, hostname, port): """Return the certificate's expiration timestamp.""" try: cert = await hass.async_add_executor_job(get_cert, hostname, port) - except socket.gaierror: - raise ResolveFailed(f"Cannot resolve hostname: {hostname}") - except socket.timeout: - raise ConnectionTimeout(f"Connection timeout with server: {hostname}:{port}") - except ConnectionRefusedError: - raise ConnectionRefused(f"Connection refused by server: {hostname}:{port}") + except socket.gaierror as err: + raise ResolveFailed(f"Cannot resolve hostname: {hostname}") from err + except socket.timeout as err: + raise ConnectionTimeout( + f"Connection timeout with server: {hostname}:{port}" + ) from err + except ConnectionRefusedError as err: + raise ConnectionRefused( + f"Connection refused by server: {hostname}:{port}" + ) from err except ssl.CertificateError as err: - raise ValidationFailure(err.verify_message) + raise ValidationFailure(err.verify_message) from err except ssl.SSLError as err: - raise ValidationFailure(err.args[0]) + raise ValidationFailure(err.args[0]) from err ts_seconds = ssl.cert_time_to_seconds(cert["notAfter"]) return dt.utc_from_timestamp(ts_seconds) diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 799fe6acc70..924ef2fa6b4 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -225,8 +225,8 @@ class CityBikesNetworks: result = network[ATTR_ID] return result - except CityBikesRequestError: - raise PlatformNotReady + except CityBikesRequestError as err: + raise PlatformNotReady from err finally: self.networks_loading.release() @@ -251,11 +251,11 @@ class CityBikesNetwork: ) self.stations = network[ATTR_NETWORK][ATTR_STATIONS_LIST] self.ready.set() - except CityBikesRequestError: + except CityBikesRequestError as err: if now is not None: self.ready.clear() else: - raise PlatformNotReady + raise PlatformNotReady from err class CityBikesStation(Entity): diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 0f27c678e59..41b71162d6c 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await account.getAccountBearerToken() except client_exceptions.ClientError as exception: _LOGGER.error("Error connecting to Control4 account API: %s", exception) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from exception except BadCredentials as exception: _LOGGER.error( "Error authenticating with Control4 account API, incorrect username or password: %s", diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index 2871da34f11..09e05791169 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -45,14 +45,14 @@ async def async_setup_entry( try: return await director_update_data(hass, entry, CONTROL4_NON_DIMMER_VAR) except C4Exception as err: - raise UpdateFailed(f"Error communicating with API: {err}") + raise UpdateFailed(f"Error communicating with API: {err}") from err async def async_update_data_dimmer(): """Fetch data from Control4 director for dimmer lights.""" try: return await director_update_data(hass, entry, CONTROL4_DIMMER_VAR) except C4Exception as err: - raise UpdateFailed(f"Error communicating with API: {err}") + raise UpdateFailed(f"Error communicating with API: {err}") from err non_dimmer_coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 49a939b8c72..65a3bf28c3b 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -114,12 +114,12 @@ async def daikin_api_setup(hass, host, key, uuid, password): device = await Appliance.factory( host, session, key=key, uuid=uuid, password=password ) - except asyncio.TimeoutError: + except asyncio.TimeoutError as err: _LOGGER.debug("Connection to %s timed out", host) - raise ConfigEntryNotReady - except ClientConnectionError: + raise ConfigEntryNotReady from err + except ClientConnectionError as err: _LOGGER.debug("ClientConnectionError to %s", host) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err except Exception: # pylint: disable=broad-except _LOGGER.error("Unexpected error creating device %s", host) return None diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 6ef68b43f64..828f65c9811 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -98,8 +98,8 @@ class DeconzGateway: self.async_connection_status_callback, ) - except CannotConnect: - raise ConfigEntryNotReady + except CannotConnect as err: + raise ConfigEntryNotReady from err except Exception as err: # pylint: disable=broad-except LOGGER.error("Error connecting with deCONZ gateway: %s", err) @@ -254,10 +254,10 @@ async def get_gateway( await deconz.initialize() return deconz - except errors.Unauthorized: + except errors.Unauthorized as err: LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) - raise AuthenticationRequired + raise AuthenticationRequired from err - except (asyncio.TimeoutError, errors.RequestError): + except (asyncio.TimeoutError, errors.RequestError) as err: LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) - raise CannotConnect + raise CannotConnect from err diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 4a24e979607..5e8df89c20d 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -58,9 +58,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): deluge_api = DelugeRPCClient(host, port, username, password) try: deluge_api.connect() - except ConnectionRefusedError: + except ConnectionRefusedError as err: _LOGGER.error("Connection to Deluge Daemon failed") - raise PlatformNotReady + raise PlatformNotReady from err dev = [] for variable in config[CONF_MONITORED_VARIABLES]: dev.append(DelugeSensor(variable, deluge_api, name)) diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 04acf6a9dd9..2aff1b5266c 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -46,9 +46,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): deluge_api = DelugeRPCClient(host, port, username, password) try: deluge_api.connect() - except ConnectionRefusedError: + except ConnectionRefusedError as err: _LOGGER.error("Connection to Deluge Daemon failed") - raise PlatformNotReady + raise PlatformNotReady from err add_entities([DelugeSwitch(deluge_api, name)]) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 33685b2bc1c..1db4af12f22 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -83,12 +83,14 @@ async def async_get_device_automation_platform( try: integration = await async_get_integration_with_requirements(hass, domain) platform = integration.get_platform(platform_name) - except IntegrationNotFound: - raise InvalidDeviceAutomationConfig(f"Integration '{domain}' not found") - except ImportError: + except IntegrationNotFound as err: + raise InvalidDeviceAutomationConfig( + f"Integration '{domain}' not found" + ) from err + except ImportError as err: raise InvalidDeviceAutomationConfig( f"Integration '{domain}' does not support device automation {automation_type}s" - ) + ) from err return platform diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index cfe1549f3c4..c955bf77096 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -50,8 +50,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data[DOMAIN]["homecontrol"] = await hass.async_add_executor_job( partial(HomeControl, gateway_id=gateway_id, url=mprm_url) ) - except ConnectionError: - raise ConfigEntryNotReady + except ConnectionError as err: + raise ConfigEntryNotReady from err for platform in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 6c4f8c071a1..c2eb9bd466d 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -43,8 +43,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) except AccountError: return False - except SessionError: - raise ConfigEntryNotReady + except SessionError as error: + raise ConfigEntryNotReady from error if not entry.options: hass.config_entries.async_update_entry( @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: return await hass.async_add_executor_job(dexcom.get_current_glucose_reading) except SessionError as error: - raise UpdateFailed(error) + raise UpdateFailed(error) from error hass.data[DOMAIN][entry.entry_id] = { COORDINATOR: DataUpdateCoordinator( diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index d01f8bd25c9..af27d19cfb0 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -59,8 +59,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await dtv.update() - except DIRECTVError: - raise ConfigEntryNotReady + except DIRECTVError as err: + raise ConfigEntryNotReady from err hass.data[DOMAIN][entry.entry_id] = dtv diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 75d88d59c32..acbd69138d4 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -182,8 +182,8 @@ async def async_setup_platform( factory = UpnpFactory(requester, disable_state_variable_validation=True) try: upnp_device = await factory.async_create_device(url) - except (asyncio.TimeoutError, aiohttp.ClientError): - raise PlatformNotReady() + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + raise PlatformNotReady() from err # wrap with DmrDevice dlna_device = DmrDevice(upnp_device, event_handler) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 8f9ef2a3ed8..43ab0c96153 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -132,10 +132,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): "Authorization rejected by DoorBird for %s@%s", username, device_ip ) return False - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err except OSError as oserr: _LOGGER.error("Failed to setup doorbird at %s: %s", device_ip, oserr) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from oserr if not status[0]: _LOGGER.error( diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 52f94116344..07b753da6ee 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -40,10 +40,10 @@ async def validate_input(hass: core.HomeAssistant, data): info = await hass.async_add_executor_job(device.info) except urllib.error.HTTPError as err: if err.code == 401: - raise InvalidAuth - raise CannotConnect - except OSError: - raise CannotConnect + raise InvalidAuth from err + raise CannotConnect from err + except OSError as err: + raise CannotConnect from err if not status[0]: raise CannotConnect diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index a8032a4f78a..7bbddb18e7b 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -81,7 +81,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= await ebox_data.async_update() except PyEboxError as exp: _LOGGER.error("Failed login: %s", exp) - raise PlatformNotReady + raise PlatformNotReady from exp sensors = [] for variable in config[CONF_MONITORED_VARIABLES]: diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index a1a7fc88086..855e62727b5 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -110,7 +110,7 @@ class EbusdData: self.value[name] = command_result except RuntimeError as err: _LOGGER.error(err) - raise RuntimeError(err) + raise RuntimeError(err) from err def write(self, call): """Call write methon on ebusd.""" diff --git a/homeassistant/components/ecobee/util.py b/homeassistant/components/ecobee/util.py index 2f5d194fec0..ac30b3bb660 100644 --- a/homeassistant/components/ecobee/util.py +++ b/homeassistant/components/ecobee/util.py @@ -8,8 +8,8 @@ def ecobee_date(date_string): """Validate a date_string as valid for the ecobee API.""" try: datetime.strptime(date_string, "%Y-%m-%d") - except ValueError: - raise vol.Invalid("Date does not match ecobee date format YYYY-MM-DD") + except ValueError as err: + raise vol.Invalid("Date does not match ecobee date format YYYY-MM-DD") from err return date_string @@ -17,6 +17,8 @@ def ecobee_time(time_string): """Validate a time_string as valid for the ecobee API.""" try: datetime.strptime(time_string, "%H:%M:%S") - except ValueError: - raise vol.Invalid("Time does not match ecobee 24-hour time format HH:MM:SS") + except ValueError as err: + raise vol.Invalid( + "Time does not match ecobee 24-hour time format HH:MM:SS" + ) from err return time_string diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index a7fe8d8a0f8..4ea90d27106 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -50,8 +50,8 @@ class EcobeeWeather(WeatherEntity): try: forecast = self.weather["forecasts"][index] return forecast[param] - except (ValueError, IndexError, KeyError): - raise ValueError + except (IndexError, KeyError) as err: + raise ValueError from err @property def name(self): diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index b1a210b8d15..243bd2913b1 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -55,8 +55,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= effects = await api.get_all_patterns() - except pyeverlights.ConnectionError: - raise PlatformNotReady + except pyeverlights.ConnectionError as err: + raise PlatformNotReady from err else: lights.append(EverLightsLight(api, pyeverlights.ZONE_1, status, effects)) diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py index 8e6020ebd8a..2dba50cccca 100644 --- a/homeassistant/components/flick_electric/config_flow.py +++ b/homeassistant/components/flick_electric/config_flow.py @@ -48,10 +48,10 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: with async_timeout.timeout(60): token = await auth.async_get_access_token() - except asyncio.TimeoutError: - raise CannotConnect() - except AuthException: - raise InvalidAuth() + except asyncio.TimeoutError as err: + raise CannotConnect() from err + except AuthException as err: + raise InvalidAuth() from err else: return token is not None diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 6bbadf0e89d..2233b4aa0c3 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -36,8 +36,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id]["client"] = client = await async_get_api( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session ) - except RequestError: - raise ConfigEntryNotReady + except RequestError as err: + raise ConfigEntryNotReady from err user_info = await client.user.get_info(include_location_info=True) diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py index 24208aa16a8..b509c894068 100644 --- a/homeassistant/components/flo/config_flow.py +++ b/homeassistant/components/flo/config_flow.py @@ -29,7 +29,7 @@ async def validate_input(hass: core.HomeAssistant, data): ) except RequestError as request_error: _LOGGER.error("Error connecting to the Flo API: %s", request_error) - raise CannotConnect + raise CannotConnect from request_error user_info = await api.user.get_info() a_location_id = user_info["locations"][0]["id"] diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 179a293ba20..1a01f45b641 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -46,7 +46,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): *[self._update_device(), self._update_consumption_data()] ) except (RequestError) as error: - raise UpdateFailed(error) + raise UpdateFailed(error) from error @property def location_id(self) -> str: diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 513f77edfb2..b9a5fc17682 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -67,8 +67,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): http_session=http_session, ) ) - except RequestException: - raise ConfigEntryNotReady + except RequestException as ex: + raise ConfigEntryNotReady from ex except Exception as ex: # pylint: disable=broad-except _LOGGER.error("Invalid credentials for flume: %s", ex) return False diff --git a/homeassistant/components/flume/config_flow.py b/homeassistant/components/flume/config_flow.py index f26be5f1e1d..80f1698a7e6 100644 --- a/homeassistant/components/flume/config_flow.py +++ b/homeassistant/components/flume/config_flow.py @@ -57,10 +57,10 @@ async def validate_input(hass: core.HomeAssistant, data): ) ) flume_devices = await hass.async_add_executor_job(FlumeDeviceList, flume_auth) - except RequestException: - raise CannotConnect - except Exception: # pylint: disable=broad-except - raise InvalidAuth + except RequestException as err: + raise CannotConnect from err + except Exception as err: # pylint: disable=broad-except + raise InvalidAuth from err if not flume_devices or not flume_devices.device_list: raise CannotConnect diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 5e7ebd90cf1..4dae0b3f7cd 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -52,8 +52,8 @@ def async_get_api_category(sensor_type): if sensor[0] == sensor_type ) ) - except StopIteration: - raise ValueError(f"Can't find category sensor type: {sensor_type}") + except StopIteration as err: + raise ValueError(f"Can't find category sensor type: {sensor_type}") from err async def async_setup(hass, config): diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index b1d6fefaa3d..3e71963a009 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -82,9 +82,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= asyncio.TimeoutError, FoobotClient.TooManyRequests, FoobotClient.InternalError, - ): + ) as err: _LOGGER.exception("Failed to connect to foobot servers") - raise PlatformNotReady + raise PlatformNotReady from err except FoobotClient.ClientError: _LOGGER.error("Failed to fetch data from foobot servers") return diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index 85e8132bf02..c0a1012f051 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error( "Connection error occurred during Garmin Connect login request: %s", err ) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err except Exception: # pylint: disable=broad-except _LOGGER.exception("Unknown error occurred during Garmin Connect login request") return False diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index c7e708e3207..ae3030ac88c 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -69,7 +69,7 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator): ClientConnectorError, InvalidSensorsData, ) as error: - raise UpdateFailed(error) + raise UpdateFailed(error) from error if not self.gios.data: raise UpdateFailed("Invalid sensors data") return self.gios.data diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index d09aa782534..5a0a1f33394 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -121,9 +121,9 @@ class GlancesData: self.available = True _LOGGER.debug("Successfully connected to Glances") - except exceptions.GlancesApiConnectionError: + except exceptions.GlancesApiConnectionError as err: _LOGGER.debug("Can not connect to Glances") - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index 3c86fae0357..48352e88543 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -52,8 +52,8 @@ async def validate_input(hass: core.HomeAssistant, data): try: api = get_api(hass, data) await api.get_data() - except glances_api.exceptions.GlancesApiConnectionError: - raise CannotConnect + except glances_api.exceptions.GlancesApiConnectionError as err: + raise CannotConnect from err class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index c69dee662b0..10adc9c61b3 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -69,7 +69,9 @@ def get_data_update_coordinator( async with async_timeout.timeout(3): return await hass.async_add_executor_job(api.info) except Exception as exception: - raise UpdateFailed(f"Error communicating with API: {exception}") + raise UpdateFailed( + f"Error communicating with API: {exception}" + ) from exception config_entry_data[DATA_UPDATE_COORDINATOR] = GogoGateDataUpdateCoordinator( hass, diff --git a/homeassistant/components/griddy/config_flow.py b/homeassistant/components/griddy/config_flow.py index 56284384ee0..675e48cc999 100644 --- a/homeassistant/components/griddy/config_flow.py +++ b/homeassistant/components/griddy/config_flow.py @@ -28,8 +28,8 @@ async def validate_input(hass: core.HomeAssistant, data): await AsyncGriddy( client_session, settlement_point=data[CONF_LOADZONE] ).async_getnow() - except (asyncio.TimeoutError, ClientError): - raise CannotConnect + except (asyncio.TimeoutError, ClientError) as err: + raise CannotConnect from err # Return info that you want to store in the config entry. return {"title": f"Load Zone {data[CONF_LOADZONE]}"} diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index bd83307afb7..ad2a074564c 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -45,5 +45,5 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): try: resp = await self._api_coro() except GuardianError as err: - raise UpdateFailed(err) + raise UpdateFailed(err) from err return resp["data"] diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 540e39f8f44..5bc4a132914 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -45,8 +45,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): name, entry.unique_id, address, activity, harmony_conf_file, delay_secs ) connected_ok = await device.connect() - except (asyncio.TimeoutError, ValueError, AttributeError): - raise ConfigEntryNotReady + except (asyncio.TimeoutError, ValueError, AttributeError) as err: + raise ConfigEntryNotReady from err if not connected_ok: raise ConfigEntryNotReady diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index fb2b1dc757c..23b91ac40bc 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -111,7 +111,7 @@ class HassIOPasswordReset(HassIOBaseAuth): await provider.async_change_password( data[ATTR_USERNAME], data[ATTR_PASSWORD] ) - except auth_ha.InvalidUser: - raise HTTPNotFound() + except auth_ha.InvalidUser as err: + raise HTTPNotFound() from err return web.Response(status=HTTP_OK) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index a76a29b2ed5..81f9fd9dd0e 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -75,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): except HeosError as error: await controller.disconnect() _LOGGER.debug("Unable to connect to controller %s: %s", host, error) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from error # Disconnect when shutting down async def disconnect_controller(event): @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): except HeosError as error: await controller.disconnect() _LOGGER.debug("Unable to retrieve players and sources: %s", error) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from error controller_manager = ControllerManager(hass, controller) await controller_manager.connect_listeners() diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py index a2412a2fed4..725b294c00f 100644 --- a/homeassistant/components/hisense_aehw4a1/__init__.py +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -22,8 +22,8 @@ def coerce_ip(value): raise vol.Invalid("Must define an IP address") try: ipaddress.IPv4Network(value) - except ValueError: - raise vol.Invalid("Not a valid IP address") + except ValueError as err: + raise vol.Invalid("Not a valid IP address") from err return value diff --git a/homeassistant/components/hlk_sw16/config_flow.py b/homeassistant/components/hlk_sw16/config_flow.py index 0a9ac79d1b7..5064b00a7ad 100644 --- a/homeassistant/components/hlk_sw16/config_flow.py +++ b/homeassistant/components/hlk_sw16/config_flow.py @@ -49,8 +49,8 @@ async def validate_input(hass: HomeAssistant, user_input): try: client = await connect_client(hass, user_input) - except asyncio.TimeoutError: - raise CannotConnect + except asyncio.TimeoutError as err: + raise CannotConnect from err try: def disconnect_callback(): @@ -62,7 +62,7 @@ async def validate_input(hass: HomeAssistant, user_input): except CannotConnect: client.disconnect_callback = None client.stop() - raise CannotConnect + raise else: client.disconnect_callback = None client.stop() diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index ecca4ed444c..41294268ae8 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -40,8 +40,8 @@ class TimePattern: if not (0 <= number <= self.maximum): raise vol.Invalid(f"must be a value between 0 and {self.maximum}") - except ValueError: - raise vol.Invalid("invalid time_pattern value") + except ValueError as err: + raise vol.Invalid("invalid time_pattern value") from err return value diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 431b05e692a..78f1d57ac93 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -92,8 +92,8 @@ class HomematicipHAP: self.config_entry.data.get(HMIPC_AUTHTOKEN), self.config_entry.data.get(HMIPC_NAME), ) - except HmipcConnectionError: - raise ConfigEntryNotReady + except HmipcConnectionError as err: + raise ConfigEntryNotReady from err except Exception as err: # pylint: disable=broad-except _LOGGER.error("Error connecting with HomematicIP Cloud: %s", err) return False @@ -247,8 +247,8 @@ class HomematicipHAP: try: await home.init(hapid) await home.get_current_state() - except HmipConnectionError: - raise HmipcConnectionError + except HmipConnectionError as err: + raise HmipcConnectionError from err home.on_update(self.async_update) home.on_create(self.async_create_entity) hass.loop.create_task(self.async_connect()) diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 0ed98f73a38..5b9fb656938 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -71,7 +71,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except OSError as msg: # occurs if horizon box is offline _LOGGER.error("Connection to %s at %s failed: %s", name, host, msg) - raise PlatformNotReady + raise PlatformNotReady from msg _LOGGER.info("Connection to %s at %s established", name, host) diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 888fa2423ad..1cb65292c7d 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -191,4 +191,4 @@ class HpIloData: hpilo.IloCommunicationError, hpilo.IloLoginFailed, ) as error: - raise ValueError(f"Unable to init HP ILO, {error}") + raise ValueError(f"Unable to init HP ILO, {error}") from error diff --git a/homeassistant/components/http/forwarded.py b/homeassistant/components/http/forwarded.py index 4d9ec69a018..bf6bb811a81 100644 --- a/homeassistant/components/http/forwarded.py +++ b/homeassistant/components/http/forwarded.py @@ -91,11 +91,11 @@ def async_setup_forwarded(app, trusted_proxies): forwarded_for_split = list(reversed(forwarded_for_headers[0].split(","))) try: forwarded_for = [ip_address(addr.strip()) for addr in forwarded_for_split] - except ValueError: + except ValueError as err: _LOGGER.error( "Invalid IP address in X-Forwarded-For: %s", forwarded_for_headers[0] ) - raise HTTPBadRequest + raise HTTPBadRequest from err # Find the last trusted index in the X-Forwarded-For list forwarded_for_index = 0 diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 7766d5a0cb9..354159f13be 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -52,7 +52,7 @@ class HomeAssistantView: msg = json.dumps(result, cls=JSONEncoder, allow_nan=False).encode("UTF-8") except (ValueError, TypeError) as err: _LOGGER.error("Unable to serialize to JSON: %s\n%s", err, result) - raise HTTPInternalServerError + raise HTTPInternalServerError from err response = web.Response( body=msg, content_type=CONTENT_TYPE_JSON, @@ -127,12 +127,12 @@ def request_handler_factory(view: HomeAssistantView, handler: Callable) -> Calla if asyncio.iscoroutine(result): result = await result - except vol.Invalid: - raise HTTPBadRequest() - except exceptions.ServiceNotFound: - raise HTTPInternalServerError() - except exceptions.Unauthorized: - raise HTTPUnauthorized() + except vol.Invalid as err: + raise HTTPBadRequest() from err + except exceptions.ServiceNotFound as err: + raise HTTPInternalServerError() from err + except exceptions.Unauthorized as err: + raise HTTPUnauthorized() from err if isinstance(result, web.StreamResponse): # The method handler returned a ready-made Response, how nice of it diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 545c980591e..3eb894552ab 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -94,9 +94,9 @@ class HueBridge: create_config_flow(hass, host) return False - except CannotConnect: + except CannotConnect as err: LOGGER.error("Error connecting to the Hue bridge at %s", host) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err except Exception: # pylint: disable=broad-except LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) @@ -269,18 +269,18 @@ async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): # Initialize bridge (and validate our username) await bridge.initialize() - except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): - raise AuthenticationRequired + except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized) as err: + raise AuthenticationRequired from err except ( asyncio.TimeoutError, client_exceptions.ClientOSError, client_exceptions.ServerDisconnectedError, client_exceptions.ContentTypeError, - ): - raise CannotConnect - except aiohue.AiohueException: + ) as err: + raise CannotConnect from err + except aiohue.AiohueException as err: LOGGER.exception("Unknown Hue linking error occurred") - raise AuthenticationRequired + raise AuthenticationRequired from err async def _update_listener(hass, entry): diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index c8d7de55ce8..9d055ad6066 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -159,11 +159,11 @@ async def async_safe_fetch(bridge, fetch_method): try: with async_timeout.timeout(4): return await bridge.async_request_call(fetch_method) - except aiohue.Unauthorized: + except aiohue.Unauthorized as err: await bridge.handle_unauthorized_error() - raise UpdateFailed("Unauthorized") + raise UpdateFailed("Unauthorized") from err except (aiohue.AiohueException,) as err: - raise UpdateFailed(f"Hue error: {err}") + raise UpdateFailed(f"Hue error: {err}") from err @callback diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index af8986e0212..263140464aa 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -61,11 +61,11 @@ class SensorManager: return await self.bridge.async_request_call( self.bridge.api.sensors.update ) - except Unauthorized: + except Unauthorized as err: await self.bridge.handle_unauthorized_error() - raise UpdateFailed("Unauthorized") + raise UpdateFailed("Unauthorized") from err except AiohueException as err: - raise UpdateFailed(f"Hue error: {err}") + raise UpdateFailed(f"Hue error: {err}") from err async def async_register_component(self, platform, async_add_entities): """Register async_add_entities methods for components.""" diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 3f78726bf30..c87097dc7af 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -128,9 +128,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): shade_data = _async_map_data_by_id( (await shades.get_resources())[SHADE_DATA] ) - except HUB_EXCEPTIONS: + except HUB_EXCEPTIONS as err: _LOGGER.error("Connection error to PowerView hub: %s", hub_address) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err if not device_info: _LOGGER.error("Unable to initialize PowerView hub: %s", hub_address) diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 1e47b9ec3fe..2bbdcfea3a6 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -33,8 +33,8 @@ async def validate_input(hass: core.HomeAssistant, data): try: async with async_timeout.timeout(10): device_info = await async_get_device_info(pv_request) - except HUB_EXCEPTIONS: - raise CannotConnect + except HUB_EXCEPTIONS as err: + raise CannotConnect from err if not device_info: raise CannotConnect diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index b043a6e9832..e2bf879e326 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -41,16 +41,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: with async_timeout.timeout(PLATFORM_TIMEOUT): api = await real_time_api(config_host, config_port) - except (IamMeterError, asyncio.TimeoutError): + except (IamMeterError, asyncio.TimeoutError) as err: _LOGGER.error("Device is not ready") - raise PlatformNotReady + raise PlatformNotReady from err async def async_update_data(): try: with async_timeout.timeout(PLATFORM_TIMEOUT): return await api.get_data() - except (IamMeterError, asyncio.TimeoutError): - raise UpdateFailed + except (IamMeterError, asyncio.TimeoutError) as err: + raise UpdateFailed from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 97ddd95f50c..d0667aab72a 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None aiohttp.client_exceptions.ClientConnectorError, ) as aio_exception: _LOGGER.warning("Exception raised while attempting to login: %s", aio_exception) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from aio_exception systems = await aqualink.get_systems() systems = list(systems.values()) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index d039b270bb8..683d59e9281 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -119,9 +119,12 @@ class IcloudAccount: api_devices = self.api.devices # Gets device owners infos user_info = api_devices.response["userInfo"] - except (PyiCloudServiceNotActivatedException, PyiCloudNoDevicesException): + except ( + PyiCloudServiceNotActivatedException, + PyiCloudNoDevicesException, + ) as err: _LOGGER.error("No iCloud device found") - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 91085ce4e64..d08be3e9127 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -92,8 +92,8 @@ class ImageStorageCollection(collection.StorageCollection): # Verify we can read the image try: image = Image.open(uploaded_file.file) - except UnidentifiedImageError: - raise vol.Invalid("Unable to identify image file") + except UnidentifiedImageError as err: + raise vol.Invalid("Unable to identify image file") from err # Reset content uploaded_file.file.seek(0) @@ -170,8 +170,8 @@ class ImageServeView(HomeAssistantView): parts = image_size.split("x", 1) width = int(parts[0]) height = int(parts[1]) - except (ValueError, IndexError): - raise web.HTTPBadRequest + except (ValueError, IndexError) as err: + raise web.HTTPBadRequest from err if not width or width != height or width not in VALID_SIZES: raise web.HTTPBadRequest diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 60939741894..db49e119235 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -324,22 +324,22 @@ def get_influx_connection(conf, test_write=False, test_read=False): try: write_api.write(bucket=bucket, record=json) except (urllib3.exceptions.HTTPError, OSError) as exc: - raise ConnectionError(CONNECTION_ERROR % exc) + raise ConnectionError(CONNECTION_ERROR % exc) from exc except ApiException as exc: if exc.status == CODE_INVALID_INPUTS: - raise ValueError(WRITE_ERROR % (json, exc)) - raise ConnectionError(CLIENT_ERROR_V2 % exc) + raise ValueError(WRITE_ERROR % (json, exc)) from exc + raise ConnectionError(CLIENT_ERROR_V2 % exc) from exc def query_v2(query, _=None): """Query V2 influx.""" try: return query_api.query(query) except (urllib3.exceptions.HTTPError, OSError) as exc: - raise ConnectionError(CONNECTION_ERROR % exc) + raise ConnectionError(CONNECTION_ERROR % exc) from exc except ApiException as exc: if exc.status == CODE_INVALID_INPUTS: - raise ValueError(QUERY_ERROR % (query, exc)) - raise ConnectionError(CLIENT_ERROR_V2 % exc) + raise ValueError(QUERY_ERROR % (query, exc)) from exc + raise ConnectionError(CLIENT_ERROR_V2 % exc) from exc def close_v2(): """Close V2 influx client.""" @@ -399,11 +399,11 @@ def get_influx_connection(conf, test_write=False, test_read=False): exceptions.InfluxDBServerError, OSError, ) as exc: - raise ConnectionError(CONNECTION_ERROR % exc) + raise ConnectionError(CONNECTION_ERROR % exc) from exc except exceptions.InfluxDBClientError as exc: if exc.code == CODE_INVALID_INPUTS: - raise ValueError(WRITE_ERROR % (json, exc)) - raise ConnectionError(CLIENT_ERROR_V1 % exc) + raise ValueError(WRITE_ERROR % (json, exc)) from exc + raise ConnectionError(CLIENT_ERROR_V1 % exc) from exc def query_v1(query, database=None): """Query V1 influx.""" @@ -414,11 +414,11 @@ def get_influx_connection(conf, test_write=False, test_read=False): exceptions.InfluxDBServerError, OSError, ) as exc: - raise ConnectionError(CONNECTION_ERROR % exc) + raise ConnectionError(CONNECTION_ERROR % exc) from exc except exceptions.InfluxDBClientError as exc: if exc.code == CODE_INVALID_INPUTS: - raise ValueError(QUERY_ERROR % (query, exc)) - raise ConnectionError(CLIENT_ERROR_V1 % exc) + raise ValueError(QUERY_ERROR % (query, exc)) from exc + raise ConnectionError(CLIENT_ERROR_V1 % exc) from exc def close_v1(): """Close the V1 Influx client.""" diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index 60e2a1088ca..eb5b0ce6091 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -147,7 +147,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): influx = get_influx_connection(config, test_read=True) except ConnectionError as exc: _LOGGER.error(exc) - raise PlatformNotReady() + raise PlatformNotReady() from exc entities = [] if CONF_QUERIES_FLUX in config: diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index 5a293fb2e52..a6c4b4627bc 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -173,8 +173,8 @@ def normalize_byte_entry_to_int(entry: [int, bytes, str]): raise ValueError("Not a valid hex code") try: entry = unhexlify(entry) - except HexError: - raise ValueError("Not a valid hex code") + except HexError as err: + raise ValueError("Not a valid hex code") from err return int.from_bytes(entry, byteorder="big") @@ -184,8 +184,8 @@ def add_device_override(config_data, new_override): address = str(Address(new_override[CONF_ADDRESS])) cat = normalize_byte_entry_to_int(new_override[CONF_CAT]) subcat = normalize_byte_entry_to_int(new_override[CONF_SUBCAT]) - except ValueError: - raise ValueError("Incorrect values") + except ValueError as err: + raise ValueError("Incorrect values") from err overrides = config_data.get(CONF_OVERRIDE, []) curr_override = {} diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 781117e8b71..57912d7d24d 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -107,9 +107,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= except IHAuthenticationError: _LOGGER.error("Invalid username or password") return - except IHConnectionError: + except IHConnectionError as ex: _LOGGER.error("Error connecting to the %s server", device_type) - raise PlatformNotReady + raise PlatformNotReady from ex ih_devices = controller.get_devices() if ih_devices: @@ -199,7 +199,7 @@ class IntesisAC(ClimateEntity): await self._controller.connect() except IHConnectionError as ex: _LOGGER.error("Exception connecting to IntesisHome: %s", ex) - raise PlatformNotReady + raise PlatformNotReady from ex @property def name(self): diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 07d1258d735..1f9a616dc4f 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -121,7 +121,7 @@ class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]): try: return await self.ipp.printer() except IPPError as error: - raise UpdateFailed(f"Invalid response from API: {error}") + raise UpdateFailed(f"Invalid response from API: {error}") from error class IPPEntity(Entity): diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 5f47c7dc372..d7ded256f73 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -174,8 +174,8 @@ class IslamicPrayerClient: try: await self.hass.async_add_executor_job(self.get_new_prayer_times) - except (exceptions.InvalidResponseError, ConnError): - raise ConfigEntryNotReady + except (exceptions.InvalidResponseError, ConnError) as err: + raise ConfigEntryNotReady from err await self.async_update() self.config_entry.add_update_listener(self.async_options_updated) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 0ed1d7e6833..bc589a3aa11 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -98,7 +98,7 @@ def _fetch_isy_configuration( webroot=webroot, ) except ValueError as err: - raise InvalidAuth(err.args[0]) + raise InvalidAuth(err.args[0]) from err return Configuration(log=_LOGGER, xml=isy_conn.get_config()) diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index d333b9f913b..080df7be6bf 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return False except aiohttp.ClientError as error: _LOGGER.error("Could not reach the JuiceNet API %s", error) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from error if not juicenet.devices: _LOGGER.error("No JuiceNet devices found for this account") diff --git a/homeassistant/components/juicenet/config_flow.py b/homeassistant/components/juicenet/config_flow.py index 3f089300025..5ea7e4f267a 100644 --- a/homeassistant/components/juicenet/config_flow.py +++ b/homeassistant/components/juicenet/config_flow.py @@ -28,10 +28,10 @@ async def validate_input(hass: core.HomeAssistant, data): await juicenet.get_devices() except TokenError as error: _LOGGER.error("Token Error %s", error) - raise InvalidAuth + raise InvalidAuth from error except aiohttp.ClientError as error: _LOGGER.error("Error connecting %s", error) - raise CannotConnect + raise CannotConnect from error # Return info that you want to store in the config entry. return {"title": "JuiceNet"} diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 88888da44f8..4e8d13c999e 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -186,8 +186,8 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: status = await get_status(self.hass, host, port) self.data[CONF_ID] = status.get("chipId", status["mac"].replace(":", "")) - except (CannotConnect, KeyError): - raise CannotConnect + except (CannotConnect, KeyError) as err: + raise CannotConnect from err else: self.data[CONF_MODEL] = status.get("model", KONN_MODEL) self.data[CONF_ACCESS_TOKEN] = "".join( diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index bd91bf3adbc..76e75159290 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -390,4 +390,4 @@ async def get_status(hass, host, port): except client.ClientError as err: _LOGGER.error("Exception trying to get panel status: %s", err) - raise CannotConnect + raise CannotConnect from err diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index f7ed2d72f16..ed93d4a9791 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -148,8 +148,8 @@ async def async_setup_entry(hass, config_entry): ) await luftdaten.async_update() hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT][config_entry.entry_id] = luftdaten - except LuftdatenError: - raise ConfigEntryNotReady + except LuftdatenError as err: + raise ConfigEntryNotReady from err hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, "sensor") diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index af5ada7d2c9..a223385d8e8 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -103,9 +103,9 @@ class MediaExtractor: try: all_media = ydl.extract_info(self.get_media_url(), process=False) - except DownloadError: + except DownloadError as err: # This exception will be logged by youtube-dl itself - raise MEDownloadException() + raise MEDownloadException() from err if "entries" in all_media: _LOGGER.warning("Playlists are not supported, looking for the first video") @@ -123,9 +123,9 @@ class MediaExtractor: try: ydl.params["format"] = query requested_stream = ydl.process_ie_result(selected_media, download=False) - except (ExtractorError, DownloadError): + except (ExtractorError, DownloadError) as err: _LOGGER.error("Could not extract stream for the query: %s", query) - raise MEQueryException() + raise MEQueryException() from err return requested_stream["url"] diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index ae994bfc396..1f5502170e4 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -83,7 +83,7 @@ class MetDataUpdateCoordinator(DataUpdateCoordinator): try: return await self.weather.fetch_data() except Exception as err: - raise UpdateFailed(f"Update failed: {err}") + raise UpdateFailed(f"Update failed: {err}") from err def track_home(self): """Start tracking changes to HA home setting.""" diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 1dc6041b535..fb120aa29c7 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -253,7 +253,7 @@ class MikrotikData: socket.timeout, ) as api_error: _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) - raise CannotConnect + raise CannotConnect from api_error except librouteros.exceptions.ProtocolError as api_error: _LOGGER.warning( "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", @@ -367,8 +367,8 @@ class MikrotikHub: api = await self.hass.async_add_executor_job( get_api, self.hass, self.config_entry.data ) - except CannotConnect: - raise ConfigEntryNotReady + except CannotConnect as api_error: + raise ConfigEntryNotReady from api_error except LoginError: return False @@ -415,5 +415,5 @@ def get_api(hass, entry): ) as api_error: _LOGGER.error("Mikrotik %s error: %s", entry[CONF_HOST], api_error) if "invalid user name or password" in str(api_error): - raise LoginError - raise CannotConnect + raise LoginError from api_error + raise CannotConnect from api_error diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 8820d08a518..01db11a04e3 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -219,7 +219,7 @@ async def webhook_call_service(hass, config_entry, data): config_entry.data[ATTR_DEVICE_NAME], ex, ) - raise HTTPBadRequest() + raise HTTPBadRequest() from ex return empty_okay_response() diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 8bf5f6f3115..9b367292fcf 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -58,8 +58,8 @@ def number(value: Any) -> Union[int, float]: try: value = float(value) return value - except (TypeError, ValueError): - raise vol.Invalid(f"invalid number {value}") + except (TypeError, ValueError) as err: + raise vol.Invalid(f"invalid number {value}") from err PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 9bceff1531c..06883ddc8a8 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -34,9 +34,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: monoprice = await hass.async_add_executor_job(get_monoprice, port) - except SerialException: + except SerialException as err: _LOGGER.error("Error connecting to Monoprice controller at %s", port) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err # double negative to handle absence of value first_run = not bool(entry.data.get(CONF_NOT_FIRST_RUN)) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index ddaafd01d8c..05158144916 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -55,9 +55,9 @@ async def validate_input(hass: core.HomeAssistant, data): """ try: await get_async_monoprice(data[CONF_PORT], hass.loop) - except SerialException: + except SerialException as err: _LOGGER.error("Error connecting to Monoprice controller") - raise CannotConnect + raise CannotConnect from err sources = _sources_from_config(data) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 568dbabd7b0..651fe48fe3d 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -21,8 +21,8 @@ def valid_topic(value: Any) -> str: value = cv.string(value) try: raw_value = value.encode("utf-8") - except UnicodeError: - raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.") + except UnicodeError as err: + raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.") from err if not raw_value: raise vol.Invalid("MQTT topic name/filter must not be empty.") if len(raw_value) > 65535: diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index fc1d374fe43..959000da3b3 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -37,8 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except InvalidCredentialsError as err: _LOGGER.error("There was an error while logging in: %s", err) return False - except MyQError: - raise ConfigEntryNotReady + except MyQError as err: + raise ConfigEntryNotReady from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 4fd267f1b21..d06f01342a1 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -28,10 +28,10 @@ async def validate_input(hass: core.HomeAssistant, data): try: await pymyq.login(data[CONF_USERNAME], data[CONF_PASSWORD], websession) - except InvalidCredentialsError: - raise InvalidAuth - except MyQError: - raise CannotConnect + except InvalidCredentialsError as err: + raise InvalidAuth from err + except MyQError as err: + raise CannotConnect from err return {"title": data[CONF_USERNAME]} diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 56c1562eea0..3bb6326f78d 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -54,8 +54,8 @@ def is_socket_address(value): try: socket.getaddrinfo(value, None) return value - except OSError: - raise vol.Invalid("Device is not a valid domain name or ip address") + except OSError as err: + raise vol.Invalid("Device is not a valid domain name or ip address") from err def get_mysensors_gateway(hass, gateway_id): diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index 2762792e133..510245ea859 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -52,9 +52,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if bulb.bulb_type != "rgblamp": _LOGGER.error("Device %s (%s) is not a myStrom bulb", host, mac) return - except MyStromConnectionError: + except MyStromConnectionError as err: _LOGGER.warning("No route to myStrom bulb: %s", host) - raise PlatformNotReady() + raise PlatformNotReady() from err async_add_entities([MyStromLight(bulb, name, mac)], True) diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index ab1207658ab..6332760f189 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -30,9 +30,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: plug = _MyStromSwitch(host) await plug.get_state() - except MyStromConnectionError: + except MyStromConnectionError as err: _LOGGER.error("No route to myStrom plug: %s", host) - raise PlatformNotReady() + raise PlatformNotReady() from err async_add_entities([MyStromSwitch(plug, name)]) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 311f5ff5f42..9775dc592fd 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -101,9 +101,9 @@ async def async_setup_entry(hass, entry): try: await hass.async_add_executor_job(hub.update_robots) - except NeatoRobotException: + except NeatoRobotException as ex: _LOGGER.debug("Failed to connect to Neato API") - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex hass.data[NEATO_LOGIN] = hub @@ -156,7 +156,7 @@ class NeatoHub: _LOGGER.error("Invalid credentials") else: _LOGGER.error("Unable to connect to Neato API") - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex self.logged_in = False return diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index dd43227a5ab..3d15e3c4d9b 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -57,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): requests.exceptions.HTTPError, ) as error: _LOGGER.error("Could not connect to the internet: %s", error) - raise PlatformNotReady() + raise PlatformNotReady() from error except RequestParametersError as error: _LOGGER.error("Could not fetch stations, please check configuration: %s", error) return diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 122f00f7b59..25dce69417c 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) except ConnectTimeout as ex: _LOGGER.error("Unable to connect to Nexia service: %s", ex) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex except HTTPError as http_ex: if ( http_ex.response.status_code >= HTTP_BAD_REQUEST @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) return False _LOGGER.error("HTTP error from Nexia service: %s", http_ex) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from http_ex async def _async_update_data(): """Fetch data from API endpoint.""" diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index d71a5470c98..b5163d88f63 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -39,15 +39,15 @@ async def validate_input(hass: core.HomeAssistant, data): await hass.async_add_executor_job(nexia_home.login) except ConnectTimeout as ex: _LOGGER.error("Unable to connect to Nexia service: %s", ex) - raise CannotConnect + raise CannotConnect from ex except HTTPError as http_ex: _LOGGER.error("HTTP error from Nexia service: %s", http_ex) if ( http_ex.response.status_code >= HTTP_BAD_REQUEST and http_ex.response.status_code < HTTP_INTERNAL_SERVER_ERROR ): - raise InvalidAuth - raise CannotConnect + raise InvalidAuth from http_ex + raise CannotConnect from http_ex if not nexia_home.get_name(): raise InvalidAuth diff --git a/homeassistant/components/nightscout/config_flow.py b/homeassistant/components/nightscout/config_flow.py index 699ce39355f..bd33bc8dcb4 100644 --- a/homeassistant/components/nightscout/config_flow.py +++ b/homeassistant/components/nightscout/config_flow.py @@ -23,8 +23,8 @@ async def _validate_input(data): try: api = NightscoutAPI(url) status = await api.get_server_status() - except (ClientError, AsyncIOTimeoutError, OSError): - raise InputValidationError("cannot_connect") + except (ClientError, AsyncIOTimeoutError, OSError) as error: + raise InputValidationError("cannot_connect") from error # Return info to be stored in the config entry. return {"title": status.name} diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index 4875e2e1e57..c5bc33b7e5e 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -31,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= await niko_data.async_update() except OSError as err: _LOGGER.error("Unable to access %s (%s)", host, err) - raise PlatformNotReady + raise PlatformNotReady from err async_add_entities( [NikoHomeControlLight(light, niko_data) for light in nhc.list_actions()], True diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 1c8fc650803..72b6ac610db 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False except NotionError as err: _LOGGER.error("Config entry failed: %s", err) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err async def async_update(): """Get the latest data from the Notion API.""" diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 4c7455be3c0..80765d88866 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -81,8 +81,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await hass.async_add_executor_job(api.authenticate) - except requests.exceptions.Timeout: - raise ConfigEntryNotReady + except requests.exceptions.Timeout as ex: + raise ConfigEntryNotReady from ex except requests.exceptions.HTTPError as ex: if ( ex.response.status_code > HTTP_BAD_REQUEST @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ): _LOGGER.error("Failed to login to nuheat: %s", ex) return False - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex except Exception as ex: # pylint: disable=broad-except _LOGGER.error("Failed to login to nuheat: %s", ex) return False diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py index bdbdfe22ea3..dc72438ce5e 100644 --- a/homeassistant/components/nuheat/config_flow.py +++ b/homeassistant/components/nuheat/config_flow.py @@ -36,27 +36,27 @@ async def validate_input(hass: core.HomeAssistant, data): try: await hass.async_add_executor_job(api.authenticate) - except requests.exceptions.Timeout: - raise CannotConnect + except requests.exceptions.Timeout as ex: + raise CannotConnect from ex except requests.exceptions.HTTPError as ex: if ( ex.response.status_code > HTTP_BAD_REQUEST and ex.response.status_code < HTTP_INTERNAL_SERVER_ERROR ): - raise InvalidAuth - raise CannotConnect + raise InvalidAuth from ex + raise CannotConnect from ex # # The underlying module throws a generic exception on login failure # - except Exception: # pylint: disable=broad-except - raise InvalidAuth + except Exception as ex: # pylint: disable=broad-except + raise InvalidAuth from ex try: thermostat = await hass.async_add_executor_job( api.get_thermostat, data[CONF_SERIAL_NUMBER] ) - except requests.exceptions.HTTPError: - raise InvalidThermostat + except requests.exceptions.HTTPError as ex: + raise InvalidThermostat from ex return {"title": thermostat.room, "serial_number": thermostat.serial_number} diff --git a/homeassistant/components/numato/__init__.py b/homeassistant/components/numato/__init__.py index e5eeaa31846..8efc56e12fe 100644 --- a/homeassistant/components/numato/__init__.py +++ b/homeassistant/components/numato/__init__.py @@ -57,8 +57,8 @@ def float_range(rng): coe = vol.Coerce(float) coe(rng[0]) coe(rng[1]) - except vol.CoerceInvalid: - raise vol.Invalid(f"Only int or float values are allowed: {rng}") + except vol.CoerceInvalid as err: + raise vol.Invalid(f"Only int or float values are allowed: {rng}") from err if len(rng) != 2: raise vol.Invalid(f"Only two numbers allowed in a range: {rng}") if rng[0] > rng[1]: @@ -70,8 +70,8 @@ def adc_port_number(num): """Validate input number to be in the range of ADC enabled ports.""" try: num = int(num) - except (ValueError): - raise vol.Invalid(f"Port numbers must be integers: {num}") + except ValueError as err: + raise vol.Invalid(f"Port numbers must be integers: {num}") from err if num not in range(1, 8): raise vol.Invalid(f"Only port numbers from 1 to 7 are ADC capable: {num}") return num diff --git a/homeassistant/components/nws/config_flow.py b/homeassistant/components/nws/config_flow.py index ebef7418d98..12ab09abaae 100644 --- a/homeassistant/components/nws/config_flow.py +++ b/homeassistant/components/nws/config_flow.py @@ -34,7 +34,7 @@ async def validate_input(hass: core.HomeAssistant, data): await nws.set_station(station) except aiohttp.ClientError as err: _LOGGER.error("Could not connect: %s", err) - raise CannotConnect + raise CannotConnect from err return {"title": nws.station} diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 7ce882e3c82..2df7cd1bb4b 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -60,7 +60,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Unable to connect to %(host)s: %(reason)s", dict(host=url, reason=ex), ) - raise PlatformNotReady + raise PlatformNotReady from ex entity = NX584Alarm(name, alarm_client, url) async_add_entities([entity]) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index a9ebd77b5a6..5161011dd4c 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv except OpenUvError as err: _LOGGER.error("Config entry failed: %s", err) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err for component in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index 0fcdd056b15..7f193bc09a1 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hub = Pencompy(host, port, boards=boards) except OSError as error: _LOGGER.error("Could not connect to pencompy: %s", error) - raise PlatformNotReady + raise PlatformNotReady from error # Add devices. devs = [] diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 5e7fc723cc4..a1aa0f819f8 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -95,14 +95,14 @@ async def async_setup_entry(hass, entry): await api.get_data() except HoleError as ex: _LOGGER.warning("Failed to connect: %s", ex) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex async def async_update_data(): """Fetch data from API endpoint.""" try: await api.get_data() except HoleError as err: - raise UpdateFailed(f"Failed to communicating with API: {err}") + raise UpdateFailed(f"Failed to communicating with API: {err}") from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 85d4b43b532..3552823677d 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass, entry): server_config[CONF_URL], error, ) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from error except ( plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized, diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index efb97f51c41..7e43a68b9e8 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -50,13 +50,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Invalid Smile ID") return False - except Smile.PlugwiseError: + except Smile.PlugwiseError as err: _LOGGER.error("Error while communicating to device") - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err - except asyncio.TimeoutError: + except asyncio.TimeoutError as err: _LOGGER.error("Timeout while connecting to Smile") - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err update_interval = timedelta(seconds=60) if api.smile_type == "power": @@ -68,8 +68,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(10): await api.full_update_device() return True - except Smile.XMLDataMissingError: - raise UpdateFailed("Smile update failed") + except Smile.XMLDataMissingError as err: + raise UpdateFailed("Smile update failed") from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index b02bb3cb92b..1f86394775a 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -48,10 +48,10 @@ async def validate_input(hass: core.HomeAssistant, data): try: await api.connect() - except Smile.InvalidAuthentication: - raise InvalidAuth - except Smile.PlugwiseError: - raise CannotConnect + except Smile.InvalidAuthentication as err: + raise InvalidAuth from err + except Smile.PlugwiseError as err: + raise CannotConnect from err return api diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 8e7596bd7e0..2a7ce4497bb 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return False except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Plum cloud: %s", ex) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = plum diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index bb41541b434..c3577982dc0 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -136,6 +136,6 @@ class PoolSenseDataUpdateCoordinator(DataUpdateCoordinator): data = await self.poolsense.get_poolsense_data() except (PoolSenseError) as error: _LOGGER.error("PoolSense query did not complete.") - raise UpdateFailed(error) + raise UpdateFailed(error) from error return data diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 9c1ea7afe53..2dc19a0771d 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -113,9 +113,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await hass.async_add_executor_job(power_wall.detect_and_pin_version) await hass.async_add_executor_job(_fetch_powerwall_data, power_wall) powerwall_data = await hass.async_add_executor_job(call_base_info, power_wall) - except PowerwallUnreachableError: + except PowerwallUnreachableError as err: http_session.close() - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err except APIChangedError as err: http_session.close() await _async_handle_api_changed_error(hass, err) @@ -133,8 +133,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return await hass.async_add_executor_job( _fetch_powerwall_data, power_wall ) - except PowerwallUnreachableError: - raise UpdateFailed("Unable to fetch data from powerwall") + except PowerwallUnreachableError as err: + raise UpdateFailed("Unable to fetch data from powerwall") from err except APIChangedError as err: await _async_handle_api_changed_error(hass, err) hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED] = True diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 8c313b79024..fd44949e386 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -25,12 +25,12 @@ async def validate_input(hass: core.HomeAssistant, data): try: await hass.async_add_executor_job(power_wall.detect_and_pin_version) site_info = await hass.async_add_executor_job(power_wall.get_site_info) - except PowerwallUnreachableError: - raise CannotConnect + except PowerwallUnreachableError as err: + raise CannotConnect from err except APIChangedError as err: # Only log the exception without the traceback _LOGGER.error(str(err)) - raise WrongVersion + raise WrongVersion from err # Return info that you want to store in the config entry. return {"title": site_info.site_name} diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 7aa83d30ae4..754f09fa199 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -70,9 +70,9 @@ def _precheck_image(image, opts): raise ValueError() try: img = Image.open(io.BytesIO(image)) - except OSError: + except OSError as err: _LOGGER.warning("Failed to open image") - raise ValueError() + raise ValueError() from err imgfmt = str(img.format) if imgfmt not in ("PNG", "JPEG"): _LOGGER.warning("Image is of unsupported type: %s", imgfmt) @@ -272,8 +272,8 @@ class ProxyCamera(Camera): image = await async_get_image(self.hass, self._proxied_camera) if not image: return None - except HomeAssistantError: - raise asyncio.CancelledError() + except HomeAssistantError as err: + raise asyncio.CancelledError() from err if self._mode == MODE_RESIZE: job = _resize_image diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 4bf982bbbce..cd67355d883 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -51,9 +51,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except LoginRequired: _LOGGER.error("Invalid authentication") return - except RequestException: + except RequestException as err: _LOGGER.error("Connection failed") - raise PlatformNotReady + raise PlatformNotReady from err name = config.get(CONF_NAME) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index b84ccb8fa5d..8310a12d51c 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -117,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # and there is not a reasonable timeout here so it can block for a long time except RACHIO_API_EXCEPTIONS as error: _LOGGER.error("Could not reach the Rachio API: %s", error) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from error # Check for Rachio controller devices if not person.controllers: diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index df9ead463f7..f262843d4ad 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -46,7 +46,7 @@ async def validate_input(hass: core.HomeAssistant, data): # Yes we really do get all these exceptions (hopefully rachiopy switches to requests) except RACHIO_API_EXCEPTIONS as error: _LOGGER.error("Could not reach the Rachio API: %s", error) - raise CannotConnect + raise CannotConnect from error # Return info that you want to store in the config entry. return {"title": username} diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 239878d0219..544a79c42df 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -144,7 +144,7 @@ async def async_setup_entry(hass, config_entry): ) except RainMachineError as err: _LOGGER.error("An error occurred: %s", err) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err else: # regenmaschine can load multiple controllers at once, but we only grab the one # we loaded above: diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index e6fd3d7bb59..c9e862a0672 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -214,7 +214,7 @@ class I2CHatsManager(threading.Thread): value = i2c_hat.di.value return (value >> channel) & 0x01 except ResponseException as ex: - raise I2CHatsException(str(ex)) + raise I2CHatsException(str(ex)) from ex def write_dq(self, address, channel, value): """Write a value to a I2C-HAT digital output.""" @@ -228,7 +228,7 @@ class I2CHatsManager(threading.Thread): try: i2c_hat.dq.channels[channel] = value except ResponseException as ex: - raise I2CHatsException(str(ex)) + raise I2CHatsException(str(ex)) from ex def read_dq(self, address, channel): """Read a value from a I2C-HAT digital output.""" @@ -242,4 +242,4 @@ class I2CHatsManager(threading.Thread): try: return i2c_hat.dq.channels[channel] except ResponseException as ex: - raise I2CHatsException(str(ex)) + raise I2CHatsException(str(ex)) from ex diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 8b0f4364e09..947a945eee7 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -90,8 +90,10 @@ def _bytearray_string(data): val = cv.string(data) try: return bytearray.fromhex(val) - except ValueError: - raise vol.Invalid("Data must be a hex string with multiple of two characters") + except ValueError as err: + raise vol.Invalid( + "Data must be a hex string with multiple of two characters" + ) from err def _ensure_device(value): diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 607e4f1937b..87b2026882c 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -24,10 +24,10 @@ async def validate_input(hass: core.HomeAssistant, data): data["password"], data.get("2fa"), ) - except MissingTokenError: - raise Require2FA - except AccessDeniedError: - raise InvalidAuth + except MissingTokenError as err: + raise Require2FA from err + except AccessDeniedError as err: + raise InvalidAuth from err return token diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 1baff67f580..b02ad6430e9 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -146,7 +146,7 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): return data except RokuError as error: - raise UpdateFailed(f"Invalid response from API: {error}") + raise UpdateFailed(f"Invalid response from API: {error}") from error class RokuEntity(Entity): diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index f2daaa0fbf8..192dc4de537 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -104,8 +104,8 @@ async def async_setup_entry(hass, config_entry): try: if not await async_connect_or_timeout(hass, roomba): return False - except CannotConnect: - raise exceptions.ConfigEntryNotReady + except CannotConnect as err: + raise exceptions.ConfigEntryNotReady from err hass.data[DOMAIN][config_entry.entry_id] = { ROOMBA_SESSION: roomba, @@ -136,14 +136,14 @@ async def async_connect_or_timeout(hass, roomba): if name: break await asyncio.sleep(1) - except RoombaConnectionError: + except RoombaConnectionError as err: _LOGGER.error("Error to connect to vacuum") - raise CannotConnect - except asyncio.TimeoutError: + raise CannotConnect from err + except asyncio.TimeoutError as err: # api looping if user or password incorrect and roomba exist await async_disconnect_or_timeout(hass, roomba) _LOGGER.error("Timeout expired") - raise CannotConnect + raise CannotConnect from err return {ROOMBA_SESSION: roomba, CONF_NAME: name} diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index cd27b33271f..3976d8985cd 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -59,9 +59,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: rtorrent = xmlrpc.client.ServerProxy(url) - except (xmlrpc.client.ProtocolError, ConnectionRefusedError): + except (xmlrpc.client.ProtocolError, ConnectionRefusedError) as ex: _LOGGER.error("Connection to rtorrent daemon failed") - raise PlatformNotReady + raise PlatformNotReady from ex dev = [] for variable in config[CONF_MONITORED_VARIABLES]: dev.append(RTorrentSensor(variable, rtorrent, name)) diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index 88630cb99db..e41c733e83a 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= api.get_thermostats, session_id ) except RequestException as err: - raise UpdateFailed(f"Error communicating with Schluter API: {err}") + raise UpdateFailed(f"Error communicating with Schluter API: {err}") from err if thermostats is None: return {} diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index f295b0d926c..17d0074c836 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -104,14 +104,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except SenseAuthenticationException: _LOGGER.error("Could not authenticate with sense server") return False - except SENSE_TIMEOUT_EXCEPTIONS: - raise ConfigEntryNotReady + except SENSE_TIMEOUT_EXCEPTIONS as err: + raise ConfigEntryNotReady from err sense_devices_data = SenseDevicesData() try: sense_discovered_devices = await gateway.get_discovered_device_data() - except SENSE_TIMEOUT_EXCEPTIONS: - raise ConfigEntryNotReady + except SENSE_TIMEOUT_EXCEPTIONS as err: + raise ConfigEntryNotReady from err trends_coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 29aa0c5380e..26276650752 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -100,9 +100,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError, pysensibo.SensiboError, - ): + ) as err: _LOGGER.exception("Failed to connect to Sensibo servers") - raise PlatformNotReady + raise PlatformNotReady from err if not devices: return diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 4829aeb49f2..f9b936898a1 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -37,8 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): device = await aioshelly.Device.create( entry.data["host"], aiohttp_client.async_get_clientsession(hass) ) - except (asyncio.TimeoutError, OSError): - raise ConfigEntryNotReady + except (asyncio.TimeoutError, OSError) as err: + raise ConfigEntryNotReady from err wrapper = hass.data[DOMAIN][entry.entry_id] = ShellyDeviceWrapper( hass, entry, device @@ -78,8 +78,8 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): try: async with async_timeout.timeout(5): return await self.device.update() - except aiocoap_error.Error: - raise update_coordinator.UpdateFailed("Error fetching data") + except aiocoap_error.Error as err: + raise update_coordinator.UpdateFailed("Error fetching data") from err @property def model(self): diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 07b4942ad34..4b843d6eebe 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -238,7 +238,7 @@ async def async_setup_entry(hass, config_entry): return False except SimplipyError as err: LOGGER.error("Config entry failed: %s", err) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err _async_save_refresh_token(hass, config_entry, api.refresh_token) diff --git a/homeassistant/components/sisyphus/light.py b/homeassistant/components/sisyphus/light.py index ce0c37174ef..6d5fa2fc835 100644 --- a/homeassistant/components/sisyphus/light.py +++ b/homeassistant/components/sisyphus/light.py @@ -20,8 +20,8 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): try: table_holder = hass.data[DATA_SISYPHUS][host] table = await table_holder.get_table() - except aiohttp.ClientError: - raise PlatformNotReady() + except aiohttp.ClientError as err: + raise PlatformNotReady() from err add_entities([SisyphusLight(table_holder.name, table)], update_before_add=True) diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index dbc350453b7..27d470ce885 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -50,8 +50,8 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): try: table_holder = hass.data[DATA_SISYPHUS][host] table = await table_holder.get_table() - except aiohttp.ClientError: - raise PlatformNotReady() + except aiohttp.ClientError as err: + raise PlatformNotReady() from err add_entities([SisyphusPlayer(table_holder.name, host, table)], True) diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index e286bfb2557..7b1c6cfa9b7 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -51,8 +51,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except SmartMeterTexasAuthError: _LOGGER.error("Username or password was not accepted") return False - except asyncio.TimeoutError: - raise ConfigEntryNotReady + except asyncio.TimeoutError as error: + raise ConfigEntryNotReady from error await smart_meter_texas_data.setup() @@ -113,7 +113,7 @@ class SmartMeterTexasData: try: await meter.read_meter(self.client) except (SmartMeterTexasAPIError, SmartMeterTexasAuthError) as error: - raise UpdateFailed(error) + raise UpdateFailed(error) from error return self.meters diff --git a/homeassistant/components/smart_meter_texas/config_flow.py b/homeassistant/components/smart_meter_texas/config_flow.py index ce62be73c41..211957dac9d 100644 --- a/homeassistant/components/smart_meter_texas/config_flow.py +++ b/homeassistant/components/smart_meter_texas/config_flow.py @@ -35,10 +35,10 @@ async def validate_input(hass: core.HomeAssistant, data): try: await client.authenticate() - except (asyncio.TimeoutError, ClientError, SmartMeterTexasAPIError): - raise CannotConnect + except (asyncio.TimeoutError, ClientError, SmartMeterTexasAPIError) as error: + raise CannotConnect from error except SmartMeterTexasAuthError as error: - raise InvalidAuth(error) + raise InvalidAuth(error) from error # Return info that you want to store in the config entry. return {"title": account.username} diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index cc6499f9c8d..2d22841660a 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -59,9 +59,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): try: await hub.async_login(username, password) - except pysmarthab.RequestFailedException: + except pysmarthab.RequestFailedException as err: _LOGGER.exception("Error while trying to reach SmartHab API") - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err # Pass hub object to child platforms hass.data[DOMAIN][entry.entry_id] = {DATA_HUB: hub} diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 48e56e74f70..974cde35faf 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -166,10 +166,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): remove_entry = True else: _LOGGER.debug(ex, exc_info=True) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex except (ClientConnectionError, RuntimeWarning) as ex: _LOGGER.debug(ex, exc_info=True) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex if remove_entry: hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py index 148360416a2..52f3a403ed1 100644 --- a/homeassistant/components/sms/config_flow.py +++ b/homeassistant/components/sms/config_flow.py @@ -27,8 +27,8 @@ async def get_imei_from_config(hass: core.HomeAssistant, data): raise CannotConnect try: imei = await gateway.get_imei_async() - except gammu.GSMError: # pylint: disable=no-member - raise CannotConnect + except gammu.GSMError as err: # pylint: disable=no-member + raise CannotConnect from err finally: await gateway.terminate_async() diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 8eb61560e63..3084aefd97c 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -64,11 +64,11 @@ class RealTimeDataEndpoint: try: api_response = await self.api.get_data() self.ready.set() - except InverterError: + except InverterError as err: if now is not None: self.ready.clear() return - raise PlatformNotReady + raise PlatformNotReady from err data = api_response.data for sensor in self.sensors: if sensor.key in data: diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 4228d6c8400..601509aa575 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -69,8 +69,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool try: await sonarr.update() - except SonarrError: - raise ConfigEntryNotReady + except SonarrError as err: + raise ConfigEntryNotReady from err undo_listener = entry.add_update_listener(_async_update_listener) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index ae633055b83..2a0bde306b7 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -77,7 +77,7 @@ async def async_setup_entry( except (SongpalException, asyncio.TimeoutError) as ex: _LOGGER.warning("[%s(%s)] Unable to connect", name, endpoint) _LOGGER.debug("Unable to get methods from songpal: %s", ex) - raise PlatformNotReady + raise PlatformNotReady from ex songpal_entity = SongpalEntity(name, device) async_add_entities([songpal_entity], True) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 3d257174328..57557d4558a 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -169,8 +169,8 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): """Update Speedtest data.""" try: return await self.hass.async_add_executor_job(self.update_data) - except (speedtest.ConfigRetrievalError, speedtest.NoMatchedServers): - raise UpdateFailed + except (speedtest.ConfigRetrievalError, speedtest.NoMatchedServers) as err: + raise UpdateFailed from err async def async_set_options(self): """Set options for entry.""" @@ -189,8 +189,8 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): """Set up SpeedTest.""" try: self.api = await self.hass.async_add_executor_job(speedtest.Speedtest) - except speedtest.ConfigRetrievalError: - raise ConfigEntryNotReady + except speedtest.ConfigRetrievalError as err: + raise ConfigEntryNotReady from err async def request_update(call): """Request update.""" diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index d28875032da..3f1452742f1 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -70,8 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: current_user = await hass.async_add_executor_job(spotify.me) - except SpotifyException: - raise ConfigEntryNotReady + except SpotifyException as err: + raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 520e3047439..476b64b9650 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -74,8 +74,8 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N stream.access_token = secrets.token_hex() stream.start() return hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(stream.access_token) - except Exception: - raise HomeAssistantError("Unable to get stream") + except Exception as err: + raise HomeAssistantError("Unable to get stream") from err async def async_setup(hass, config): diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 37e92ff5b4f..9927db80a65 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -95,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if ex.response.status_code > 400 and ex.response.status_code < 500: _LOGGER.error("Failed to login to tado: %s", ex) return False - raise ConfigEntryNotReady + raise ConfigEntryNotReady from ex # Do first update await hass.async_add_executor_job(tadoconnector.update) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index fb60b820ab9..0c45cc809af 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -30,14 +30,14 @@ async def validate_input(hass: core.HomeAssistant, data): Tado, data[CONF_USERNAME], data[CONF_PASSWORD] ) tado_me = await hass.async_add_executor_job(tado.getMe) - except KeyError: - raise InvalidAuth - except RuntimeError: - raise CannotConnect + except KeyError as ex: + raise InvalidAuth from ex + except RuntimeError as ex: + raise CannotConnect from ex except requests.exceptions.HTTPError as ex: if ex.response.status_code > 400 and ex.response.status_code < 500: - raise InvalidAuth - raise CannotConnect + raise InvalidAuth from ex + raise CannotConnect from ex if "homes" not in tado_me or len(tado_me["homes"]) == 0: raise NoHomes diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index d78b5eb2641..b6c80de69b7 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -35,8 +35,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Fetch data from API endpoint.""" try: return await tankerkoenig.fetch_data() - except LookupError: - raise UpdateFailed("Failed to fetch data") + except LookupError as err: + raise UpdateFailed("Failed to fetch data") from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index bc2a09788e5..9b8962ae196 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -244,7 +244,7 @@ class TeslaDataUpdateCoordinator(DataUpdateCoordinator): async with async_timeout.timeout(30): return await self.controller.update() except TeslaException as err: - raise UpdateFailed(f"Error communicating with API: {err}") + raise UpdateFailed(f"Error communicating with API: {err}") from err class TeslaDevice(Entity): diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index f9a218d2f5f..9e46a30972f 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -142,9 +142,9 @@ async def validate_input(hass: core.HomeAssistant, data): except TeslaException as ex: if ex.code == 401: _LOGGER.error("Invalid credentials: %s", ex) - raise InvalidAuth() + raise InvalidAuth() from ex _LOGGER.error("Unable to communicate with Tesla API: %s", ex) - raise CannotConnect() + raise CannotConnect() from ex _LOGGER.debug("Credentials successfully connected to the Tesla API") return config diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 657be67c7fc..b4b29a84297 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -64,8 +64,8 @@ async def async_setup_entry(hass, entry): try: await tibber_connection.update_info() - except asyncio.TimeoutError: - raise ConfigEntryNotReady + except asyncio.TimeoutError as err: + raise ConfigEntryNotReady from err except aiohttp.ClientError as err: _LOGGER.error("Error connecting to Tibber: %s ", err) return False diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 323267cae6f..939c6d1597d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -32,10 +32,10 @@ async def async_setup_entry(hass, entry, async_add_entities): await home.update_info() except asyncio.TimeoutError as err: _LOGGER.error("Timeout connecting to Tibber home: %s ", err) - raise PlatformNotReady() + raise PlatformNotReady() from err except aiohttp.ClientError as err: _LOGGER.error("Error connecting to Tibber home: %s ", err) - raise PlatformNotReady() + raise PlatformNotReady() from err if home.has_active_subscription: dev.append(TibberSensorElPrice(home)) if home.has_real_time_consumption: diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 4f6411ed368..58295c98bef 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass, config_entry): 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}") + raise UpdateFailed(f"Error while retrieving data: {err}") from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py index fa4cf52a630..359cb5b0ffb 100644 --- a/homeassistant/components/toon/coordinator.py +++ b/homeassistant/components/toon/coordinator.py @@ -141,4 +141,4 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): try: return await self.toon.update() except ToonError as error: - raise UpdateFailed(f"Invalid response from API: {error}") + raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index cef22c636c1..7074532e097 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -113,9 +113,9 @@ async def async_setup_entry(hass, entry): try: gateway_info = await api(gateway.get_gateway_info()) - except RequestError: + except RequestError as err: await factory.shutdown() - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err hass.data.setdefault(KEY_API, {})[entry.entry_id] = api hass.data.setdefault(KEY_GATEWAY, {})[entry.entry_id] = gateway diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index e438fd20170..e72291c8d88 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -166,10 +166,10 @@ async def authenticate(hass, host, security_code): try: with async_timeout.timeout(5): key = await api_factory.generate_psk(security_code) - except RequestError: - raise AuthError("invalid_security_code") - except asyncio.TimeoutError: - raise AuthError("timeout") + except RequestError as err: + raise AuthError("invalid_security_code") from err + except asyncio.TimeoutError as err: + raise AuthError("timeout") from err return await get_gateway_info(hass, host, identity, key) @@ -185,10 +185,10 @@ async def get_gateway_info(hass, host, identity, key): gateway_info_result = await api(gateway.get_gateway_info()) await factory.shutdown() - except (OSError, RequestError): + except (OSError, RequestError) as err: # We're also catching OSError as PyTradfri doesn't catch that one yet # Upstream PR: https://github.com/ggravlingen/pytradfri/pull/189 - raise AuthError("cannot_connect") + raise AuthError("cannot_connect") from err return { CONF_HOST: host, diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 2fd7e60cf31..00fc2d1b3b5 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -136,13 +136,13 @@ async def get_api(hass, entry): except TransmissionError as error: if "401: Unauthorized" in str(error): _LOGGER.error("Credentials for Transmission client are not valid") - raise AuthenticationError + raise AuthenticationError from error if "111: Connection refused" in str(error): _LOGGER.error("Connecting to the Transmission client %s failed", host) - raise CannotConnect + raise CannotConnect from error _LOGGER.error(error) - raise UnknownError + raise UnknownError from error class TransmissionClient: @@ -166,8 +166,8 @@ class TransmissionClient: try: self.tm_api = await get_api(self.hass, self.config_entry.data) - except CannotConnect: - raise ConfigEntryNotReady + except CannotConnect as error: + raise ConfigEntryNotReady from error except (AuthenticationError, UnknownError): return False diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index d1e2d910790..0f758d4a2eb 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -254,14 +254,14 @@ class SpeechManager: _init_tts_cache_dir, self.hass, cache_dir ) except OSError as err: - raise HomeAssistantError(f"Can't init cache dir {err}") + raise HomeAssistantError(f"Can't init cache dir {err}") from err try: cache_files = await self.hass.async_add_executor_job( _get_cache_files, self.cache_dir ) except OSError as err: - raise HomeAssistantError(f"Can't read cache dir {err}") + raise HomeAssistantError(f"Can't read cache dir {err}") from err if cache_files: self.file_cache.update(cache_files) @@ -408,9 +408,9 @@ class SpeechManager: try: data = await self.hass.async_add_executor_job(load_speech) - except OSError: + except OSError as err: del self.file_cache[key] - raise HomeAssistantError(f"Can't read {voice_file}") + raise HomeAssistantError(f"Can't read {voice_file}") from err self._async_store_to_memcache(key, filename, data) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 6524c026fcf..9d8bb873836 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -95,8 +95,8 @@ async def async_setup_entry(hass, entry): await hass.async_add_executor_job( tuya.init, username, password, country_code, platform ) - except (TuyaNetException, TuyaServerException): - raise ConfigEntryNotReady() + except (TuyaNetException, TuyaServerException) as exc: + raise ConfigEntryNotReady() from exc except TuyaAPIException as exc: _LOGGER.error( diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 3a3229415ab..7c30a34f58f 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -295,8 +295,8 @@ class UniFiController: description = await self.api.site_description() self._site_role = description[0]["site_role"] - except CannotConnect: - raise ConfigEntryNotReady + except CannotConnect as err: + raise ConfigEntryNotReady from err except Exception as err: # pylint: disable=broad-except LOGGER.error("Unknown error connecting with UniFi controller: %s", err) @@ -428,14 +428,14 @@ async def get_controller( await controller.login() return controller - except aiounifi.Unauthorized: + except aiounifi.Unauthorized as err: LOGGER.warning("Connected to UniFi at %s but not registered.", host) - raise AuthenticationRequired + raise AuthenticationRequired from err - except (asyncio.TimeoutError, aiounifi.RequestError): + except (asyncio.TimeoutError, aiounifi.RequestError) as err: LOGGER.error("Error connecting to the UniFi controller at %s", host) - raise CannotConnect + raise CannotConnect from err - except aiounifi.AiounifiException: + except aiounifi.AiounifiException as err: LOGGER.exception("Unknown UniFi communication error occurred") - raise AuthenticationRequired + raise AuthenticationRequired from err diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index f3c9483e4a8..3d7b8b626c9 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -148,13 +148,15 @@ async def get_newest_version(hass, huuid, include_components): try: res = await req.json() - except ValueError: + except ValueError as err: raise update_coordinator.UpdateFailed( "Received invalid JSON from Home Assistant Update" - ) + ) from err try: res = RESPONSE_SCHEMA(res) return res["version"], res["release-notes"] except vol.Invalid as err: - raise update_coordinator.UpdateFailed(f"Got unexpected response: {err}") + raise update_coordinator.UpdateFailed( + f"Got unexpected response: {err}" + ) from err diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 8479bd06518..52cada89333 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -109,8 +109,8 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name try: device = await async_discover_and_construct(hass, udn, st) - except asyncio.TimeoutError: - raise ConfigEntryNotReady + except asyncio.TimeoutError as err: + raise ConfigEntryNotReady from err if not device: _LOGGER.info("Unable to create UPnP/IGD, aborting") diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 3e0363f4f35..e07fac28d1f 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -58,10 +58,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return False except nvr.NvrError as ex: _LOGGER.error("NVR refuses to talk to me: %s", str(ex)) - raise PlatformNotReady + raise PlatformNotReady from ex except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to NVR: %s", str(ex)) - raise PlatformNotReady + raise PlatformNotReady from ex add_entities( [ diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 72ffda48b57..a859567e219 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): controller.scan(callback) except velbus.util.VelbusException as err: _LOGGER.error("An error occurred: %s", err) - raise ConfigEntryNotReady + raise ConfigEntryNotReady from err def syn_clock(self, service=None): try: diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 90e55ea08a0..ec18880b5ba 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -84,9 +84,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= & set(station_filter) ): dev.append(waqi_sensor) - except (aiohttp.client_exceptions.ClientConnectorError, asyncio.TimeoutError): + except ( + aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, + ) as err: _LOGGER.exception("Failed to connect to WAQI servers") - raise PlatformNotReady + raise PlatformNotReady from err async_add_entities(dev, True) diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index f5b29f49b1e..3c795902900 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -62,7 +62,7 @@ class AuthPhase: ) self._logger.warning(error_msg) self._send_message(auth_invalid_message(error_msg)) - raise Disconnect + raise Disconnect from err if "access_token" in msg: self._logger.debug("Received access_token") diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index ab412e06583..7c56fcbc606 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -180,9 +180,9 @@ class WebSocketHandler: try: with async_timeout.timeout(10): msg = await wsock.receive() - except asyncio.TimeoutError: + except asyncio.TimeoutError as err: disconnect_warn = "Did not receive auth message within 10 seconds" - raise Disconnect + raise Disconnect from err if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): raise Disconnect @@ -193,9 +193,9 @@ class WebSocketHandler: try: msg_data = msg.json() - except ValueError: + except ValueError as err: disconnect_warn = "Received invalid JSON." - raise Disconnect + raise Disconnect from err self._logger.debug("Received %s", msg_data) connection = await auth.async_handle(msg_data) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 76b61dd7808..5da55d0e3bd 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -136,7 +136,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): try: return await self.wled.update(full_update=not self.last_update_success) except WLEDError as error: - raise UpdateFailed(f"Invalid response from API: {error}") + raise UpdateFailed(f"Invalid response from API: {error}") from error class WLEDEntity(Entity): diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index cce9d542446..9a272c502a0 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -53,9 +53,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): values = await wolf_client.fetch_value(gateway_id, device_id, parameters) return {v.value_id: v.value for v in values} except ConnectError as exception: - raise UpdateFailed(f"Error communicating with API: {exception}") - except InvalidAuth: - raise UpdateFailed("Invalid authentication during update.") + raise UpdateFailed( + f"Error communicating with API: {exception}" + ) from exception + except InvalidAuth as exception: + raise UpdateFailed("Invalid authentication during update.") from exception coordinator = DataUpdateCoordinator( hass, @@ -98,6 +100,6 @@ async def fetch_parameters(client: WolfClient, gateway_id: int, device_id: int): fetched_parameters = await client.fetch_parameters(gateway_id, device_id) return [param for param in fetched_parameters if param.name != "Reglertyp"] except ConnectError as exception: - raise UpdateFailed(f"Error communicating with API: {exception}") - except InvalidAuth: - raise UpdateFailed("Invalid authentication during update.") + raise UpdateFailed(f"Error communicating with API: {exception}") from exception + except InvalidAuth as exception: + raise UpdateFailed("Invalid authentication during update.") from exception diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 1613f10d66a..8f8b794515e 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -36,10 +36,10 @@ def valid_country(value: Any) -> str: try: raw_value = value.encode("utf-8") - except UnicodeError: + except UnicodeError as err: raise vol.Invalid( "The country name or the abbreviation must be a valid UTF-8 string." - ) + ) from err if not raw_value: raise vol.Invalid("Country name or the abbreviation must not be empty.") if value not in all_supported_countries: diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 7da4da9c05d..baeb0bf39e5 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -53,8 +53,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: device_info = await hass.async_add_executor_job(miio_device.info) - except DeviceException: - raise PlatformNotReady + except DeviceException as ex: + raise PlatformNotReady from ex model = device_info.model unique_id = f"{model}-{device_info.mac_address}" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 6be4831b6e0..3382ca910c3 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -523,8 +523,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_info.firmware_version, device_info.hardware_version, ) - except DeviceException: - raise PlatformNotReady + except DeviceException as ex: + raise PlatformNotReady from ex if model in PURIFIER_MIOT: air_purifier = AirPurifierMiot(host, token) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 967ea8043f3..a148b98ee22 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -150,8 +150,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_info.firmware_version, device_info.hardware_version, ) - except DeviceException: - raise PlatformNotReady + except DeviceException as ex: + raise PlatformNotReady from ex if model == "philips.light.sread1": light = PhilipsEyecare(host, token) diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 9c8d06e0b0d..c88b2d3663c 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -86,7 +86,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) except DeviceException as ex: _LOGGER.error("Device unavailable or token incorrect: %s", ex) - raise PlatformNotReady + raise PlatformNotReady from ex if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 4b1442a8c55..924d6d8a23d 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -122,8 +122,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_info.hardware_version, ) device = XiaomiAirQualityMonitor(name, air_quality_monitor, model, unique_id) - except DeviceException: - raise PlatformNotReady + except DeviceException as ex: + raise PlatformNotReady from ex hass.data[DATA_KEY][host] = device async_add_entities([device], update_before_add=True) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index d8552244ce8..26ffb7e578f 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -139,8 +139,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_info.firmware_version, device_info.hardware_version, ) - except DeviceException: - raise PlatformNotReady + except DeviceException as ex: + raise PlatformNotReady from ex if model in ["chuangmi.plug.v1", "chuangmi.plug.v3"]: plug = ChuangmiPlug(host, token, model=model) diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index 4273b5294ed..e669f530197 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -90,7 +90,7 @@ class YiCamera(Camera): await ftp.connect(self.host) await ftp.login(self.user, self.passwd) except (ConnectionRefusedError, StatusCodeError) as err: - raise PlatformNotReady(err) + raise PlatformNotReady(err) from err try: await ftp.change_directory(self.path) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 1ba9ada5413..bdfeb7815c5 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -316,8 +316,8 @@ def cv_group_member(value: Any) -> GroupMember: group_member = GroupMember( ieee=EUI64.convert(value["ieee"]), endpoint_id=value["endpoint_id"] ) - except KeyError: - raise vol.Invalid("Not a group member") + except KeyError as err: + raise vol.Invalid("Not a group member") from err return group_member @@ -724,8 +724,8 @@ def is_cluster_binding(value: Any) -> ClusterBinding: id=value["id"], endpoint_id=value["endpoint_id"], ) - except KeyError: - raise vol.Invalid("Not a cluster binding") + except KeyError as err: + raise vol.Invalid("Not a cluster binding") from err return cluster_binding diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index e92d0fb3028..9d04d36f748 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -29,8 +29,8 @@ async def async_validate_trigger_config(hass, config): trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError): - raise InvalidDeviceAutomationConfig + except (KeyError, AttributeError) as err: + raise InvalidDeviceAutomationConfig from err if ( zha_device.device_automation_triggers is None or trigger not in zha_device.device_automation_triggers diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 878d162c2ff..f708f138f88 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -516,9 +516,9 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): """ try: integration = await loader.async_get_integration(self.hass, handler_key) - except loader.IntegrationNotFound: + except loader.IntegrationNotFound as err: _LOGGER.error("Cannot find integration %s", handler_key) - raise data_entry_flow.UnknownHandler + raise data_entry_flow.UnknownHandler from err # Make sure requirements and dependencies of component are resolved await async_process_deps_reqs(self.hass, self._hass_config, integration) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 46b309815ab..394500b5170 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -173,8 +173,8 @@ def isdevice(value: Any) -> str: try: os.stat(value) return str(value) - except OSError: - raise vol.Invalid(f"No device at {value} found") + except OSError as err: + raise vol.Invalid(f"No device at {value} found") from err def matches_regex(regex: str) -> Callable[[Any], str]: @@ -201,12 +201,12 @@ def is_regex(value: Any) -> Pattern[Any]: try: r = re.compile(value) return r - except TypeError: + except TypeError as err: raise vol.Invalid( f"value {value} is of the wrong type for a regular expression" - ) - except re.error: - raise vol.Invalid(f"value {value} is not a valid regular expression") + ) from err + except re.error as err: + raise vol.Invalid(f"value {value} is not a valid regular expression") from err def isfile(value: Any) -> str: @@ -331,8 +331,8 @@ def time(value: Any) -> time_sys: try: time_val = dt_util.parse_time(value) - except TypeError: - raise vol.Invalid("Not a parseable type") + except TypeError as err: + raise vol.Invalid("Not a parseable type") from err if time_val is None: raise vol.Invalid(f"Invalid time specified: {value}") @@ -347,8 +347,8 @@ def date(value: Any) -> date_sys: try: date_val = dt_util.parse_date(value) - except TypeError: - raise vol.Invalid("Not a parseable type") + except TypeError as err: + raise vol.Invalid("Not a parseable type") from err if date_val is None: raise vol.Invalid("Could not parse date") @@ -380,8 +380,8 @@ def time_period_str(value: str) -> timedelta: second = float(parsed[2]) except IndexError: second = 0 - except ValueError: - raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) + except ValueError as err: + raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) from err offset = timedelta(hours=hour, minutes=minute, seconds=second) @@ -395,8 +395,8 @@ def time_period_seconds(value: Union[float, str]) -> timedelta: """Validate and transform seconds to a time offset.""" try: return timedelta(seconds=float(value)) - except (ValueError, TypeError): - raise vol.Invalid(f"Expected seconds, got {value}") + except (ValueError, TypeError) as err: + raise vol.Invalid(f"Expected seconds, got {value}") from err time_period = vol.Any(time_period_str, time_period_seconds, timedelta, time_period_dict) @@ -525,7 +525,7 @@ def template(value: Optional[Any]) -> template_helper.Template: template_value.ensure_valid() return cast(template_helper.Template, template_value) except TemplateError as ex: - raise vol.Invalid(f"invalid template ({ex})") + raise vol.Invalid(f"invalid template ({ex})") from ex def dynamic_template(value: Optional[Any]) -> template_helper.Template: @@ -543,7 +543,7 @@ def dynamic_template(value: Optional[Any]) -> template_helper.Template: template_value.ensure_valid() return cast(template_helper.Template, template_value) except TemplateError as ex: - raise vol.Invalid(f"invalid template ({ex})") + raise vol.Invalid(f"invalid template ({ex})") from ex def template_complex(value: Any) -> Any: diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 35f7b3fab9f..def2508ff92 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -54,9 +54,11 @@ def report(what: str) -> None: """ try: integration_frame = get_integration_frame() - except MissingIntegrationFrame: + except MissingIntegrationFrame as err: # Did not source from an integration? Hard error. - raise RuntimeError(f"Detected code that {what}. Please report this issue.") + raise RuntimeError( + f"Detected code that {what}. Please report this issue." + ) from err report_integration(what, integration_frame) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 471cabd0032..d40fd9fad2b 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -199,8 +199,8 @@ def _get_cloud_url(hass: HomeAssistant, require_current_request: bool = False) - if "cloud" in hass.config.components: try: cloud_url = yarl.URL(cast(str, hass.components.cloud.async_remote_ui_url())) - except hass.components.cloud.CloudNotAvailable: - raise NoURLAvailableError + except hass.components.cloud.CloudNotAvailable as err: + raise NoURLAvailableError from err if not require_current_request or cloud_url.host == _get_request_host(): return normalize_url(str(cloud_url)) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 7148f633dfb..ad0536ff3f9 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -264,7 +264,7 @@ class _ScriptRun: ex, level=logging.ERROR, ) - raise _StopScript + raise _StopScript from ex async def _async_delay_step(self): """Handle delay.""" @@ -327,10 +327,10 @@ class _ScriptRun: try: async with timeout(delay) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - except asyncio.TimeoutError: + except asyncio.TimeoutError as ex: if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): self._log(_TIMEOUT_MSG) - raise _StopScript + raise _StopScript from ex self._variables["wait"]["remaining"] = 0.0 finally: for task in tasks: @@ -502,7 +502,7 @@ class _ScriptRun: ex, level=logging.ERROR, ) - raise _StopScript + raise _StopScript from ex extra_msg = f" of {count}" for iteration in range(1, count + 1): set_repeat_var(iteration, count) @@ -600,10 +600,10 @@ class _ScriptRun: try: async with timeout(delay) as to_context: await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) - except asyncio.TimeoutError: + except asyncio.TimeoutError as ex: if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): self._log(_TIMEOUT_MSG) - raise _StopScript + raise _StopScript from ex self._variables["wait"]["remaining"] = 0.0 finally: for task in tasks: diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 2403967e3cf..fddd32c8760 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -232,7 +232,7 @@ class Template: try: self._compiled_code = self._env.compile(self.template) except jinja2.exceptions.TemplateSyntaxError as err: - raise TemplateError(err) + raise TemplateError(err) from err def extract_entities( self, variables: TemplateVarsType = None @@ -272,7 +272,7 @@ class Template: try: return compiled.render(kwargs).strip() except jinja2.TemplateError as err: - raise TemplateError(err) + raise TemplateError(err) from err @callback def async_render_to_info( diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 487d97ffdd8..8d0235413db 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -45,7 +45,7 @@ def color(the_color, *args, reset=None): return parse_colors(the_color) return parse_colors(the_color) + " ".join(args) + escape_codes[reset or "reset"] except KeyError as k: - raise ValueError(f"Invalid color {k!s} in {the_color}") + raise ValueError(f"Invalid color {k!s} in {the_color}") from k def run(script_args: List) -> int: diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 7b6da837c49..e906462a250 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -35,10 +35,10 @@ def load_json( _LOGGER.debug("JSON file not found: %s", filename) except ValueError as error: _LOGGER.exception("Could not parse JSON content: %s", filename) - raise HomeAssistantError(error) + raise HomeAssistantError(error) from error except OSError as error: _LOGGER.exception("JSON file reading failed: %s", filename) - raise HomeAssistantError(error) + raise HomeAssistantError(error) from error return {} if default is None else default @@ -55,10 +55,10 @@ def save_json( """ try: json_data = json.dumps(data, indent=4, cls=encoder) - except TypeError: + except TypeError as error: msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data))}" _LOGGER.error(msg) - raise SerializationError(msg) + raise SerializationError(msg) from error tmp_filename = "" tmp_path = os.path.split(filename)[0] @@ -74,7 +74,7 @@ def save_json( os.replace(tmp_filename, filename) except OSError as error: _LOGGER.exception("Saving JSON file failed: %s", filename) - raise WriteError(error) + raise WriteError(error) from error finally: if os.path.exists(tmp_filename): try: diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 8635de00fe4..496ca377936 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -70,7 +70,7 @@ def object_to_yaml(data: JSON_TYPE) -> str: return result except YAMLError as exc: _LOGGER.error("YAML error: %s", exc) - raise HomeAssistantError(exc) + raise HomeAssistantError(exc) from exc def yaml_to_object(data: str) -> JSON_TYPE: @@ -81,7 +81,7 @@ def yaml_to_object(data: str) -> JSON_TYPE: return result except YAMLError as exc: _LOGGER.error("YAML error: %s", exc) - raise HomeAssistantError(exc) + raise HomeAssistantError(exc) from exc def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: @@ -102,10 +102,10 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: return yaml.load(conf_file) or OrderedDict() except YAMLError as exc: _LOGGER.error("YAML error in %s: %s", fname, exc) - raise HomeAssistantError(exc) + raise HomeAssistantError(exc) from exc except UnicodeDecodeError as exc: _LOGGER.error("Unable to read file %s: %s", fname, exc) - raise HomeAssistantError(exc) + raise HomeAssistantError(exc) from exc def save_yaml(fname: str, data: JSON_TYPE) -> None: @@ -132,10 +132,10 @@ def save_yaml(fname: str, data: JSON_TYPE) -> None: pass except YAMLError as exc: _LOGGER.error(str(exc)) - raise HomeAssistantError(exc) + raise HomeAssistantError(exc) from exc except OSError as exc: _LOGGER.exception("Saving YAML file %s failed: %s", fname, exc) - raise WriteError(exc) + raise WriteError(exc) from exc finally: if os.path.exists(tmp_fname): try: diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index a58480e26b7..7e954f21e1a 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -61,10 +61,10 @@ def load_yaml(fname: str) -> JSON_TYPE: return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict() except yaml.YAMLError as exc: _LOGGER.error(str(exc)) - raise HomeAssistantError(exc) + raise HomeAssistantError(exc) from exc except UnicodeDecodeError as exc: _LOGGER.error("Unable to read file %s: %s", fname, exc) - raise HomeAssistantError(exc) + raise HomeAssistantError(exc) from exc @overload @@ -109,8 +109,10 @@ def _include_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: fname = os.path.join(os.path.dirname(loader.name), node.value) try: return _add_reference(load_yaml(fname), loader, node) - except FileNotFoundError: - raise HomeAssistantError(f"{node.start_mark}: Unable to read file {fname}.") + except FileNotFoundError as exc: + raise HomeAssistantError( + f"{node.start_mark}: Unable to read file {fname}." + ) from exc def _is_file_valid(name: str) -> bool: @@ -195,12 +197,12 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order try: hash(key) - except TypeError: + except TypeError as exc: fname = getattr(loader.stream, "name", "") raise yaml.MarkedYAMLError( context=f'invalid key: "{key}"', context_mark=yaml.Mark(fname, 0, line, -1, None, None), - ) + ) from exc if key in seen: fname = getattr(loader.stream, "name", "") From 7c191388a9181c8e98e6dc28fd85b34ce4c984f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 08:50:09 -0500 Subject: [PATCH 398/862] Use icmplib for ping when available (#39284) * Use icmplib for ping when available * Update homeassistant/components/ping/binary_sensor.py Co-authored-by: Paulus Schoutsen * Revert "Update homeassistant/components/ping/binary_sensor.py" This reverts commit 618f42512a89834bb8b2ed7830e7e77d79a29f44. * move it up so its easier to see Co-authored-by: Paulus Schoutsen --- .../components/ping/binary_sensor.py | 44 +++++++++++++++++-- homeassistant/components/ping/const.py | 1 + .../components/ping/device_tracker.py | 43 ++++++++++++++++-- homeassistant/components/ping/manifest.json | 1 + requirements_all.txt | 3 ++ 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 0c568bb7892..535c5bcd6a2 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -6,6 +6,7 @@ import re import sys from typing import Any, Dict +from icmplib import SocketPermissionError, ping as icmp_ping import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity @@ -59,7 +60,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None) -> None: count = config[CONF_PING_COUNT] name = config.get(CONF_NAME, f"{DEFAULT_NAME} {host}") - add_entities([PingBinarySensor(name, PingData(host, count))], True) + try: + # Verify we can create a raw socket, or + # fallback to using a subprocess + icmp_ping("127.0.0.1", count=0, timeout=0) + ping_cls = PingDataICMPLib + except SocketPermissionError: + ping_cls = PingDataSubProcess + + ping_data = ping_cls(hass, host, count) + + add_entities([PingBinarySensor(name, ping_data)], True) class PingBinarySensor(BinarySensorEntity): @@ -102,15 +113,42 @@ class PingBinarySensor(BinarySensorEntity): class PingData: - """The Class for handling the data retrieval.""" + """The base class for handling the data retrieval.""" - def __init__(self, host, count) -> None: + def __init__(self, hass, host, count) -> None: """Initialize the data object.""" + self.hass = hass self._ip_address = host self._count = count self.data = {} self.available = False + +class PingDataICMPLib(PingData): + """The Class for handling the data retrieval using icmplib.""" + + def ping(self): + """Send ICMP echo request and return details.""" + return icmp_ping(self._ip_address, count=self._count) + + async def async_update(self) -> None: + """Retrieve the latest details from the host.""" + data = await self.hass.async_add_executor_job(self.ping) + self.data = { + "min": data.min_rtt, + "max": data.max_rtt, + "avg": data.avg_rtt, + "mdev": "", + } + self.available = data.is_alive + + +class PingDataSubProcess(PingData): + """The Class for handling the data retrieval using the ping binary.""" + + def __init__(self, hass, host, count) -> None: + """Initialize the data object.""" + super().__init__(hass, host, count) if sys.platform == "win32": self._ping_cmd = [ "ping", diff --git a/homeassistant/components/ping/const.py b/homeassistant/components/ping/const.py index 8be8c1bdaa3..89b93c84169 100644 --- a/homeassistant/components/ping/const.py +++ b/homeassistant/components/ping/const.py @@ -1,3 +1,4 @@ """Tracks devices by sending a ICMP echo request (ping).""" PING_TIMEOUT = 3 +PING_ATTEMPTS_COUNT = 3 diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index f4e2e806143..cbbce13171b 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -4,6 +4,7 @@ import logging import subprocess import sys +from icmplib import SocketPermissionError, ping as icmp_ping import voluptuous as vol from homeassistant import const, util @@ -16,7 +17,7 @@ from homeassistant.components.device_tracker.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util.process import kill_subprocess -from .const import PING_TIMEOUT +from .const import PING_ATTEMPTS_COUNT, PING_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -class Host: +class HostSubProcess: """Host object with ping detection.""" def __init__(self, ip_address, dev_id, hass, config): @@ -72,10 +73,46 @@ class Host: _LOGGER.debug("No response from %s failed=%d", self.ip_address, failed) +class HostICMPLib: + """Host object with ping detection.""" + + def __init__(self, ip_address, dev_id, _, config): + """Initialize the Host pinger.""" + self.ip_address = ip_address + self.dev_id = dev_id + self._count = config[CONF_PING_COUNT] + + def ping(self): + """Send an ICMP echo request and return True if success.""" + return icmp_ping(self.ip_address, count=PING_ATTEMPTS_COUNT).is_alive + + def update(self, see): + """Update device state by sending one or more ping messages.""" + if self.ping(): + see(dev_id=self.dev_id, source_type=SOURCE_TYPE_ROUTER) + return True + + _LOGGER.debug( + "No response from %s (%s) failed=%d", + self.ip_address, + self.dev_id, + PING_ATTEMPTS_COUNT, + ) + + def setup_scanner(hass, config, see, discovery_info=None): """Set up the Host objects and return the update function.""" + + try: + # Verify we can create a raw socket, or + # fallback to using a subprocess + icmp_ping("127.0.0.1", count=0, timeout=0) + host_cls = HostICMPLib + except SocketPermissionError: + host_cls = HostSubProcess + hosts = [ - Host(ip, dev_id, hass, config) + host_cls(ip, dev_id, hass, config) for (dev_id, ip) in config[const.CONF_HOSTS].items() ] interval = config.get( diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index 887b48dbaae..f23697808a2 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -3,5 +3,6 @@ "name": "Ping (ICMP)", "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], + "requirements": ["icmplib==1.1.1"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 96aecbde2ac..bb7f1aa0960 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -784,6 +784,9 @@ ibm-watson==4.0.1 # homeassistant.components.watson_iot ibmiotf==0.3.4 +# homeassistant.components.ping +icmplib==1.1.1 + # homeassistant.components.iglo iglo==1.2.7 From f8704a2dfc25105c841c340570e36dbafe1a6878 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 09:27:25 -0500 Subject: [PATCH 399/862] Ensure we always fire time pattern changes after microsecond 0 (#39302) --- homeassistant/helpers/event.py | 35 ++++++++++++++++++++++++++++------ tests/util/test_dt.py | 4 ++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index cd436919997..21f49c008c8 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -34,6 +34,8 @@ from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe +MAX_TIME_TRACKING_ERROR = 0.001 + TRACK_STATE_CHANGE_CALLBACKS = "track_state_change_callbacks" TRACK_STATE_CHANGE_LISTENER = "track_state_change_listener" @@ -996,19 +998,40 @@ def async_track_utc_time_change( calculate_next(now + timedelta(seconds=1)) - # We always get time.time() first to avoid time.time() - # ticking forward after fetching hass.loop.time() - # and callback being scheduled a few microseconds early cancel_callback = hass.loop.call_at( - -time.time() + hass.loop.time() + next_time.timestamp(), + -time.time() + + hass.loop.time() + + next_time.timestamp() + + MAX_TIME_TRACKING_ERROR, pattern_time_change_listener, ) # We always get time.time() first to avoid time.time() # ticking forward after fetching hass.loop.time() - # and callback being scheduled a few microseconds early + # and callback being scheduled a few microseconds early. + # + # Since we loose additional time calling `hass.loop.time()` + # we add MAX_TIME_TRACKING_ERROR to ensure + # we always schedule the call within the time window between + # second and the next second. + # + # For example: + # If the clock ticks forward 30 microseconds when fectching + # `hass.loop.time()` and we want the event to fire at exactly + # 03:00:00.000000, the event would actually fire around + # 02:59:59.999970. To ensure we always fire sometime between + # 03:00:00.000000 and 03:00:00.999999 we add + # MAX_TIME_TRACKING_ERROR to make up for the time + # lost fetching the time. This ensures we do not fire the + # event before the next time pattern match which would result + # in the event being fired again since we would otherwise + # potentially fire early. + # cancel_callback = hass.loop.call_at( - -time.time() + hass.loop.time() + next_time.timestamp(), + -time.time() + + hass.loop.time() + + next_time.timestamp() + + MAX_TIME_TRACKING_ERROR, pattern_time_change_listener, ) diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index f693f3026c5..03d4ee53cbe 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -219,6 +219,10 @@ def test_find_next_time_expression_time_basic(): datetime(2018, 10, 7, 10, 30, 0), 5, 0, 0 ) + assert find(datetime(2018, 10, 7, 10, 30, 0, 999999), "*", "/30", 0) == datetime( + 2018, 10, 7, 10, 30, 0 + ) + def test_find_next_time_expression_time_dst(): """Test daylight saving time for find_next_time_expression_time.""" From 2109444ba53d238a90edf2833b30ef340cc4fa25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 09:32:05 -0500 Subject: [PATCH 400/862] Update time triggers to use async_track_state_change_event (#39338) This was one of the ones missed in the async_track_state_change to async_track_state_change_event conversion. --- .../components/homeassistant/triggers/time.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 76acaf89c6c..01eb1a9b85f 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -9,7 +9,7 @@ from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import ( async_track_point_in_time, - async_track_state_change, + async_track_state_change_event, async_track_time_change, ) import homeassistant.util.dt as dt_util @@ -43,7 +43,13 @@ async def async_attach_trigger(hass, config, action, automation_info): hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}}) @callback - def update_entity_trigger(entity_id, old_state=None, new_state=None): + def update_entity_trigger_event(event): + """update_entity_trigger from the event.""" + return update_entity_trigger(event.data["entity_id"], event.data["new_state"]) + + @callback + def update_entity_trigger(entity_id, new_state=None): + """Update the entity trigger for the entity_id.""" # If a listener was already set up for entity, remove it. remove = entities.get(entity_id) if remove: @@ -111,7 +117,9 @@ async def async_attach_trigger(hass, config, action, automation_info): # Track state changes of any entities. removes.append( - async_track_state_change(hass, list(entities), update_entity_trigger) + async_track_state_change_event( + hass, list(entities), update_entity_trigger_event + ) ) @callback From 400741006b75f0e91010f6b3a236fa07cb4c86bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 09:44:51 -0500 Subject: [PATCH 401/862] Add the ability to reload generic platforms from yaml (#39289) --- homeassistant/components/generic/__init__.py | 3 + homeassistant/components/generic/camera.py | 6 ++ .../components/generic/services.yaml | 2 + tests/components/generic/test_camera.py | 64 ++++++++++++++++++- tests/fixtures/generic/configuration.yaml | 6 ++ 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/generic/services.yaml create mode 100644 tests/fixtures/generic/configuration.yaml diff --git a/homeassistant/components/generic/__init__.py b/homeassistant/components/generic/__init__.py index 28f79fc91e6..feb51d0177d 100644 --- a/homeassistant/components/generic/__init__.py +++ b/homeassistant/components/generic/__init__.py @@ -1 +1,4 @@ """The generic component.""" + +DOMAIN = "generic" +PLATFORMS = ["camera"] diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 91f5322ae81..a7ef367a95d 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -26,6 +26,9 @@ from homeassistant.const import ( from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.reload import async_setup_reload_service + +from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -59,6 +62,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a generic IP Camera.""" + + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + async_add_entities([GenericCamera(hass, config)]) diff --git a/homeassistant/components/generic/services.yaml b/homeassistant/components/generic/services.yaml new file mode 100644 index 00000000000..afde1990cef --- /dev/null +++ b/homeassistant/components/generic/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all generic entities. diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index fffa5db6be5..c3400d8f57c 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -1,8 +1,15 @@ """The tests for generic camera component.""" import asyncio +from os import path +from homeassistant import config as hass_config +from homeassistant.components.generic import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR, HTTP_NOT_FOUND +from homeassistant.const import ( + HTTP_INTERNAL_SERVER_ERROR, + HTTP_NOT_FOUND, + SERVICE_RELOAD, +) from homeassistant.setup import async_setup_component from tests.async_mock import patch @@ -292,3 +299,58 @@ async def test_camera_content_type(aioclient_mock, hass, hass_client): assert resp_2.content_type == "image/jpeg" body = await resp_2.text() assert body == svg_image + + +async def test_reloading(aioclient_mock, hass, hass_client): + """Test we can cleanly reload.""" + aioclient_mock.get("http://example.com", text="hello world") + + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + "username": "user", + "password": "pass", + } + }, + ) + await hass.async_block_till_done() + + client = await hass_client() + + resp = await client.get("/api/camera_proxy/camera.config_test") + + assert resp.status == 200 + assert aioclient_mock.call_count == 1 + body = await resp.text() + assert body == "hello world" + + yaml_path = path.join( + _get_fixtures_base_path(), "fixtures", "generic/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, {}, blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + resp = await client.get("/api/camera_proxy/camera.config_test") + + assert resp.status == 404 + + resp = await client.get("/api/camera_proxy/camera.reload") + + assert resp.status == 200 + assert aioclient_mock.call_count == 2 + body = await resp.text() + assert body == "hello world" + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/generic/configuration.yaml b/tests/fixtures/generic/configuration.yaml new file mode 100644 index 00000000000..7ed443a3fb1 --- /dev/null +++ b/tests/fixtures/generic/configuration.yaml @@ -0,0 +1,6 @@ +camera: + - platform: generic + name: reload + still_image_url: "http://example.com" + username: user + password: pass From 414a59ae9f8f14507603a85a5481661ad8371e6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 09:46:45 -0500 Subject: [PATCH 402/862] Add the ability to reload homekit from yaml (#39326) --- homeassistant/components/homekit/__init__.py | 76 ++++++++++++++----- .../components/homekit/services.yaml | 3 + homeassistant/helpers/reload.py | 13 +++- tests/components/homekit/test_homekit.py | 68 +++++++++++++++++ .../helpers/test_domain_configuration.yaml | 4 + tests/fixtures/homekit/configuration.yaml | 3 + tests/helpers/test_reload.py | 33 ++++++++ 7 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 tests/fixtures/helpers/test_domain_configuration.yaml create mode 100644 tests/fixtures/homekit/configuration.yaml diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index d1c909cf2b0..e8360b1d73b 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -30,12 +30,14 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, + SERVICE_RELOAD, ) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA +from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.loader import async_get_integration from homeassistant.util import get_local_ip @@ -150,23 +152,7 @@ async def async_setup(hass: HomeAssistant, config: dict): entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries} for index, conf in enumerate(config[DOMAIN]): - bridge_name = conf[CONF_NAME] - - if ( - bridge_name in entries_by_name - and entries_by_name[bridge_name].source == SOURCE_IMPORT - ): - entry = entries_by_name[bridge_name] - # If they alter the yaml config we import the changes - # since there currently is no practical way to support - # all the options in the UI at this time. - data = conf.copy() - options = {} - for key in CONFIG_OPTIONS: - options[key] = data[key] - del data[key] - - hass.config_entries.async_update_entry(entry, data=data, options=options) + if _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): continue conf[CONF_ENTRY_INDEX] = index @@ -181,6 +167,36 @@ async def async_setup(hass: HomeAssistant, config: dict): return True +@callback +def _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): + """Update a config entry with the latest yaml. + + Returns True if a matching config entry was found + + Returns False if there is no matching config entry + """ + bridge_name = conf[CONF_NAME] + + if ( + bridge_name in entries_by_name + and entries_by_name[bridge_name].source == SOURCE_IMPORT + ): + entry = entries_by_name[bridge_name] + # If they alter the yaml config we import the changes + # since there currently is no practical way to support + # all the options in the UI at this time. + data = conf.copy() + options = {} + for key in CONFIG_OPTIONS: + options[key] = data[key] + del data[key] + + hass.config_entries.async_update_entry(entry, data=data, options=options) + return True + + return False + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up HomeKit from a config entry.""" _async_import_options_from_data_if_missing(hass, entry) @@ -349,6 +365,32 @@ def _async_register_events_and_services(hass: HomeAssistant): DOMAIN, SERVICE_HOMEKIT_START, async_handle_homekit_service_start ) + async def _handle_homekit_reload(service): + """Handle start HomeKit service call.""" + config = await async_integration_yaml_config(hass, DOMAIN) + + if not config or DOMAIN not in config: + return + + current_entries = hass.config_entries.async_entries(DOMAIN) + entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries} + + for conf in config[DOMAIN]: + _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf) + + reload_tasks = [ + hass.config_entries.async_reload(entry.entry_id) + for entry in current_entries + ] + + await asyncio.gather(*reload_tasks) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_RELOAD, + _handle_homekit_reload, + ) + class HomeKit: """Class to handle all actions between HomeKit and Home Assistant.""" diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml index b33ba642c8d..c2dde2cac6c 100644 --- a/homeassistant/components/homekit/services.yaml +++ b/homeassistant/components/homekit/services.yaml @@ -3,6 +3,9 @@ start: description: Starts the HomeKit driver. +reload: + description: Reload homekit and re-process yaml configuration. + reset_accessory: description: Reset a HomeKit accessory. This can be useful when changing a media_player’s device class to tv, linking a battery, or whenever Home Assistant adds support for new HomeKit features to existing entities. fields: diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 4ff9198f233..1ffba25ce15 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Iterable, Optional +from typing import Any, Dict, Iterable, Optional from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD @@ -59,6 +59,17 @@ async def async_reload_integration_platforms( await platform.async_setup(p_config) # type: ignore +async def async_integration_yaml_config( + hass: HomeAssistantType, integration_name: str +) -> Optional[Dict[Any, Any]]: + """Fetch the latest yaml configuration for an integration.""" + integration = await async_get_integration(hass, integration_name) + + return await conf_util.async_process_component_config( + hass, await conf_util.async_hass_config_yaml(hass), integration + ) + + @callback def async_get_platform( hass: HomeAssistantType, integration_name: str, integration_platform_name: str diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index c20b2a9d9fb..94c5c28ecb2 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -4,6 +4,7 @@ from typing import Dict import pytest +from homeassistant import config as hass_config from homeassistant.components import zeroconf from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, @@ -49,6 +50,7 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + SERVICE_RELOAD, STATE_ON, UNIT_PERCENTAGE, ) @@ -1181,3 +1183,69 @@ async def test_homekit_finds_linked_humidity_sensors( "linked_humidity_sensor": "sensor.humidifier_humidity_sensor", }, ) + + +async def test_reload(hass): + """Test we can reload from yaml.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={CONF_NAME: "reloadable", CONF_PORT: 12345}, + options={}, + ) + entry.add_to_hass(hass) + + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component( + hass, "homekit", {"homekit": {CONF_NAME: "reloadable", CONF_PORT: 12345}} + ) + await hass.async_block_till_done() + + mock_homekit.assert_any_call( + hass, + "reloadable", + 12345, + None, + ANY, + {}, + DEFAULT_SAFE_MODE, + None, + entry.entry_id, + ) + assert mock_homekit().setup.called is True + yaml_path = os.path.join( + _get_fixtures_base_path(), + "fixtures", + "homekit/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( + f"{PATH_HOMEKIT}.HomeKit" + ) as mock_homekit2: + mock_homekit2.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + await hass.services.async_call( + "homekit", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_homekit2.assert_any_call( + hass, + "reloadable", + 45678, + None, + ANY, + {}, + DEFAULT_SAFE_MODE, + None, + entry.entry_id, + ) + assert mock_homekit2().setup.called is True + + +def _get_fixtures_base_path(): + return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) diff --git a/tests/fixtures/helpers/test_domain_configuration.yaml b/tests/fixtures/helpers/test_domain_configuration.yaml new file mode 100644 index 00000000000..3c9c6ed8a7b --- /dev/null +++ b/tests/fixtures/helpers/test_domain_configuration.yaml @@ -0,0 +1,4 @@ +test_domain: + - name: one + - name: two + diff --git a/tests/fixtures/homekit/configuration.yaml b/tests/fixtures/homekit/configuration.yaml new file mode 100644 index 00000000000..546c4a1a09f --- /dev/null +++ b/tests/fixtures/homekit/configuration.yaml @@ -0,0 +1,3 @@ +homekit: + - name: reloadable + port: 45678 diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index 255d3cde40a..dafcbebdb6e 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -2,11 +2,14 @@ import logging from os import path +import pytest + from homeassistant import config from homeassistant.const import SERVICE_RELOAD from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.reload import ( async_get_platform, + async_integration_yaml_config, async_reload_integration_platforms, async_setup_reload_service, ) @@ -106,5 +109,35 @@ async def test_setup_reload_service(hass): assert len(setup_called) == 2 +async def test_async_integration_yaml_config(hass): + """Test loading yaml config for an integration.""" + mock_integration(hass, MockModule(DOMAIN)) + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + f"helpers/{DOMAIN}_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + processed_config = await async_integration_yaml_config(hass, DOMAIN) + + assert processed_config == {DOMAIN: [{"name": "one"}, {"name": "two"}]} + + +async def test_async_integration_missing_yaml_config(hass): + """Test loading missing yaml config for an integration.""" + mock_integration(hass, MockModule(DOMAIN)) + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "helpers/does_not_exist_configuration.yaml", + ) + with pytest.raises(FileNotFoundError), patch.object( + config, "YAML_CONFIG_FILE", yaml_path + ): + await async_integration_yaml_config(hass, DOMAIN) + + def _get_fixtures_base_path(): return path.dirname(path.dirname(__file__)) From 5217139e0b384fd6b39de537715b1be9df739eab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Aug 2020 16:49:17 +0200 Subject: [PATCH 403/862] Allow exposing domains in cloud (#39216) --- .../components/cloud/alexa_config.py | 40 +++++++++----- homeassistant/components/cloud/const.py | 17 +++++- .../components/cloud/google_config.py | 26 ++++++--- homeassistant/components/cloud/http_api.py | 8 ++- homeassistant/components/cloud/prefs.py | 53 ++++++++++++++----- tests/components/cloud/test_alexa_config.py | 11 +++- tests/components/cloud/test_google_config.py | 42 ++++++++++++--- tests/components/cloud/test_http_api.py | 39 ++++++++++++++ 8 files changed, 193 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 3afb0ce2e86..1bb74053ea4 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -14,18 +14,13 @@ from homeassistant.components.alexa import ( state_report as alexa_state_report, ) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST -from homeassistant.core import callback +from homeassistant.core import callback, split_entity_id from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow -from .const import ( - CONF_ENTITY_CONFIG, - CONF_FILTER, - DEFAULT_SHOULD_EXPOSE, - PREF_SHOULD_EXPOSE, - RequireRelink, -) +from .const import CONF_ENTITY_CONFIG, CONF_FILTER, PREF_SHOULD_EXPOSE, RequireRelink +from .prefs import CloudPreferences _LOGGER = logging.getLogger(__name__) @@ -37,7 +32,7 @@ SYNC_DELAY = 1 class AlexaConfig(alexa_config.AbstractConfig): """Alexa Configuration.""" - def __init__(self, hass, config, prefs, cloud): + def __init__(self, hass, config, prefs: CloudPreferences, cloud): """Initialize the Alexa config.""" super().__init__(hass) self._config = config @@ -46,6 +41,7 @@ class AlexaConfig(alexa_config.AbstractConfig): self._token = None self._token_valid = None self._cur_entity_prefs = prefs.alexa_entity_configs + self._cur_default_expose = prefs.alexa_default_expose self._alexa_sync_unsub = None self._endpoint = None @@ -99,7 +95,17 @@ class AlexaConfig(alexa_config.AbstractConfig): entity_configs = self._prefs.alexa_entity_configs entity_config = entity_configs.get(entity_id, {}) - return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + entity_expose = entity_config.get(PREF_SHOULD_EXPOSE) + if entity_expose is not None: + return entity_expose + + default_expose = self._prefs.alexa_default_expose + + # Backwards compat + if default_expose is None: + return True + + return split_entity_id(entity_id)[0] in default_expose @callback def async_invalidate_access_token(self): @@ -147,16 +153,24 @@ class AlexaConfig(alexa_config.AbstractConfig): await self.async_sync_entities() return - # If entity prefs are the same or we have filter in config.yaml, - # don't sync. + # If user has filter in config.yaml, don't sync. + if not self._config[CONF_FILTER].empty_filter: + return + + # If entity prefs are the same, don't sync. if ( self._cur_entity_prefs is prefs.alexa_entity_configs - or not self._config[CONF_FILTER].empty_filter + and self._cur_default_expose is prefs.alexa_default_expose ): return if self._alexa_sync_unsub: self._alexa_sync_unsub() + self._alexa_sync_unsub = None + + if self._cur_default_expose is not prefs.alexa_default_expose: + await self.async_sync_entities() + return self._alexa_sync_unsub = async_call_later( self.hass, SYNC_DELAY, self._sync_prefs diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 3d930f0c2e5..3c7804970fb 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -18,10 +18,25 @@ PREF_ALIASES = "aliases" PREF_SHOULD_EXPOSE = "should_expose" PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" PREF_USERNAME = "username" -DEFAULT_SHOULD_EXPOSE = True +PREF_ALEXA_DEFAULT_EXPOSE = "alexa_default_expose" +PREF_GOOGLE_DEFAULT_EXPOSE = "google_default_expose" DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False DEFAULT_GOOGLE_REPORT_STATE = False +DEFAULT_EXPOSED_DOMAINS = [ + "climate", + "cover", + "fan", + "humidifier", + "light", + "lock", + "scene", + "script", + "sensor", + "switch", + "vacuum", + "water_heater", +] CONF_ALEXA = "alexa" CONF_ALIASES = "aliases" diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 9b94b77ca45..882124af45c 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -11,16 +11,16 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, HTTP_OK, ) -from homeassistant.core import CoreState, callback +from homeassistant.core import CoreState, callback, split_entity_id from homeassistant.helpers import entity_registry from .const import ( CONF_ENTITY_CONFIG, DEFAULT_DISABLE_2FA, - DEFAULT_SHOULD_EXPOSE, PREF_DISABLE_2FA, PREF_SHOULD_EXPOSE, ) +from .prefs import CloudPreferences _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, hass, config, cloud_user, prefs, cloud): + def __init__(self, hass, config, cloud_user, prefs: CloudPreferences, cloud): """Initialize the Google config.""" super().__init__(hass) self._config = config @@ -36,6 +36,7 @@ class CloudGoogleConfig(AbstractConfig): self._prefs = prefs self._cloud = cloud self._cur_entity_prefs = self._prefs.google_entity_configs + self._cur_default_expose = self._prefs.google_default_expose self._sync_entities_lock = asyncio.Lock() self._sync_on_started = False @@ -104,7 +105,17 @@ class CloudGoogleConfig(AbstractConfig): entity_configs = self._prefs.google_entity_configs entity_config = entity_configs.get(entity_id, {}) - return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + entity_expose = entity_config.get(PREF_SHOULD_EXPOSE) + if entity_expose is not None: + return entity_expose + + default_expose = self._prefs.google_default_expose + + # Backwards compat + if default_expose is None: + return True + + return split_entity_id(entity_id)[0] in default_expose @property def agent_user_id(self): @@ -153,8 +164,8 @@ class CloudGoogleConfig(AbstractConfig): # don't sync. elif ( self._cur_entity_prefs is not prefs.google_entity_configs - and self._config["filter"].empty_filter - ): + or self._cur_default_expose is not prefs.google_default_expose + ) and self._config["filter"].empty_filter: self.async_schedule_google_sync_all() if self.enabled and not self.is_local_sdk_active: @@ -162,6 +173,9 @@ class CloudGoogleConfig(AbstractConfig): elif not self.enabled and self.is_local_sdk_active: self.async_disable_local_sdk() + self._cur_entity_prefs = prefs.google_entity_configs + self._cur_default_expose = prefs.google_default_expose + async def _handle_entity_registry_updated(self, event): """Handle when entity registry updated.""" if not self.enabled or not self._cloud.is_logged_in: diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 6589f2b43f5..00a2ddb4663 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -24,9 +24,11 @@ from homeassistant.core import callback from .const import ( DOMAIN, + PREF_ALEXA_DEFAULT_EXPOSE, PREF_ALEXA_REPORT_STATE, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, + PREF_GOOGLE_DEFAULT_EXPOSE, PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_SECURE_DEVICES_PIN, REQUEST_TIMEOUT, @@ -371,6 +373,8 @@ async def websocket_subscription(hass, connection, msg): vol.Optional(PREF_ENABLE_ALEXA): bool, vol.Optional(PREF_ALEXA_REPORT_STATE): bool, vol.Optional(PREF_GOOGLE_REPORT_STATE): bool, + vol.Optional(PREF_ALEXA_DEFAULT_EXPOSE): [str], + vol.Optional(PREF_GOOGLE_DEFAULT_EXPOSE): [str], vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), } ) @@ -514,7 +518,7 @@ async def google_assistant_list(hass, connection, msg): { "type": "cloud/google_assistant/entities/update", "entity_id": str, - vol.Optional("should_expose"): bool, + vol.Optional("should_expose"): vol.Any(None, bool), vol.Optional("override_name"): str, vol.Optional("aliases"): [str], vol.Optional("disable_2fa"): bool, @@ -566,7 +570,7 @@ async def alexa_list(hass, connection, msg): { "type": "cloud/alexa/entities/update", "entity_id": str, - vol.Optional("should_expose"): bool, + vol.Optional("should_expose"): vol.Any(None, bool), } ) async def alexa_update(hass, connection, msg): diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index a7d1b59fd39..0a41f8e2a8f 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,6 +1,6 @@ """Preference management for cloud.""" from ipaddress import ip_address -from typing import Optional +from typing import List, Optional from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import User @@ -9,8 +9,10 @@ from homeassistant.util.logging import async_create_catching_coro from .const import ( DEFAULT_ALEXA_REPORT_STATE, + DEFAULT_EXPOSED_DOMAINS, DEFAULT_GOOGLE_REPORT_STATE, DOMAIN, + PREF_ALEXA_DEFAULT_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, PREF_ALEXA_REPORT_STATE, PREF_ALIASES, @@ -20,6 +22,7 @@ from .const import ( PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, + PREF_GOOGLE_DEFAULT_EXPOSE, PREF_GOOGLE_ENTITY_CONFIGS, PREF_GOOGLE_LOCAL_WEBHOOK_ID, PREF_GOOGLE_REPORT_STATE, @@ -81,6 +84,8 @@ class CloudPreferences: alexa_entity_configs=_UNDEF, alexa_report_state=_UNDEF, google_report_state=_UNDEF, + alexa_default_expose=_UNDEF, + google_default_expose=_UNDEF, ): """Update user preferences.""" prefs = {**self._prefs} @@ -96,6 +101,8 @@ class CloudPreferences: (PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs), (PREF_ALEXA_REPORT_STATE, alexa_report_state), (PREF_GOOGLE_REPORT_STATE, google_report_state), + (PREF_ALEXA_DEFAULT_EXPOSE, alexa_default_expose), + (PREF_GOOGLE_DEFAULT_EXPOSE, google_default_expose), ): if value is not _UNDEF: prefs[key] = value @@ -185,15 +192,17 @@ class CloudPreferences: def as_dict(self): """Return dictionary version.""" return { + PREF_ALEXA_DEFAULT_EXPOSE: self.alexa_default_expose, + PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs, + PREF_ALEXA_REPORT_STATE: self.alexa_report_state, + PREF_CLOUDHOOKS: self.cloudhooks, PREF_ENABLE_ALEXA: self.alexa_enabled, PREF_ENABLE_GOOGLE: self.google_enabled, PREF_ENABLE_REMOTE: self.remote_enabled, - PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, + PREF_GOOGLE_DEFAULT_EXPOSE: self.google_default_expose, PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs, - PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs, - PREF_ALEXA_REPORT_STATE: self.alexa_report_state, PREF_GOOGLE_REPORT_STATE: self.google_report_state, - PREF_CLOUDHOOKS: self.cloudhooks, + PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, } @property @@ -219,6 +228,19 @@ class CloudPreferences: """Return if Alexa report state is enabled.""" return self._prefs.get(PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE) + @property + def alexa_default_expose(self) -> Optional[List[str]]: + """Return array of entity domains that are exposed by default to Alexa. + + Can return None, in which case for backwards should be interpreted as allow all domains. + """ + return self._prefs.get(PREF_ALEXA_DEFAULT_EXPOSE) + + @property + def alexa_entity_configs(self): + """Return Alexa Entity configurations.""" + return self._prefs.get(PREF_ALEXA_ENTITY_CONFIGS, {}) + @property def google_enabled(self): """Return if Google is enabled.""" @@ -245,9 +267,12 @@ class CloudPreferences: return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID] @property - def alexa_entity_configs(self): - """Return Alexa Entity configurations.""" - return self._prefs.get(PREF_ALEXA_ENTITY_CONFIGS, {}) + def google_default_expose(self) -> Optional[List[str]]: + """Return array of entity domains that are exposed by default to Google. + + Can return None, in which case for backwards should be interpreted as allow all domains. + """ + return self._prefs.get(PREF_GOOGLE_DEFAULT_EXPOSE) @property def cloudhooks(self): @@ -322,14 +347,16 @@ class CloudPreferences: def _empty_config(self, username): """Return an empty config.""" return { + PREF_ALEXA_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS, + PREF_ALEXA_ENTITY_CONFIGS: {}, + PREF_CLOUD_USER: None, + PREF_CLOUDHOOKS: {}, PREF_ENABLE_ALEXA: True, PREF_ENABLE_GOOGLE: True, PREF_ENABLE_REMOTE: False, - PREF_GOOGLE_SECURE_DEVICES_PIN: None, + PREF_GOOGLE_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS, PREF_GOOGLE_ENTITY_CONFIGS: {}, - PREF_ALEXA_ENTITY_CONFIGS: {}, - PREF_CLOUDHOOKS: {}, - PREF_CLOUD_USER: None, - PREF_USERNAME: username, PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(), + PREF_GOOGLE_SECURE_DEVICES_PIN: None, + PREF_USERNAME: username, } diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index b064a5c9605..cbeda41dac7 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -12,13 +12,22 @@ from tests.common import async_fire_time_changed async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): """Test Alexa config should expose using prefs.""" entity_conf = {"should_expose": False} - await cloud_prefs.async_update(alexa_entity_configs={"light.kitchen": entity_conf}) + await cloud_prefs.async_update( + alexa_entity_configs={"light.kitchen": entity_conf}, + alexa_default_expose=["light"], + ) conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) assert not conf.should_expose("light.kitchen") entity_conf["should_expose"] = True assert conf.should_expose("light.kitchen") + entity_conf["should_expose"] = None + assert conf.should_expose("light.kitchen") + + await cloud_prefs.async_update(alexa_default_expose=["sensor"],) + assert not conf.should_expose("light.kitchen") + async def test_alexa_config_report_state(hass, cloud_prefs): """Test Alexa config should expose using prefs.""" diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 5dd4afe883c..977df95051e 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,9 +1,11 @@ """Test the Cloud Google Config.""" +import pytest + from homeassistant.components.cloud import GACTIONS_SCHEMA from homeassistant.components.cloud.google_config import CloudGoogleConfig from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, HTTP_NOT_FOUND -from homeassistant.core import CoreState +from homeassistant.core import CoreState, State from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow @@ -11,19 +13,24 @@ from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed -async def test_google_update_report_state(hass, cloud_prefs): - """Test Google config responds to updating preference.""" - config = CloudGoogleConfig( +@pytest.fixture +def mock_conf(hass, cloud_prefs): + """Mock Google conf.""" + return CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, Mock(claims={"cognito:username": "abcdefghjkl"}), ) - await config.async_initialize() - await config.async_connect_agent_user("mock-user-id") - with patch.object(config, "async_sync_entities") as mock_sync, patch( + +async def test_google_update_report_state(mock_conf, hass, cloud_prefs): + """Test Google config responds to updating preference.""" + await mock_conf.async_initialize() + await mock_conf.async_connect_agent_user("mock-user-id") + + with patch.object(mock_conf, "async_sync_entities") as mock_sync, patch( "homeassistant.components.google_assistant.report_state.async_enable_report_state" ) as mock_report_state: await cloud_prefs.async_update(google_report_state=True) @@ -161,3 +168,24 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert len(mock_sync.mock_calls) == 1 + + +async def test_google_config_expose_entity_prefs(mock_conf, cloud_prefs): + """Test Google config should expose using prefs.""" + entity_conf = {"should_expose": False} + await cloud_prefs.async_update( + google_entity_configs={"light.kitchen": entity_conf}, + google_default_expose=["light"], + ) + + state = State("light.kitchen", "on") + + assert not mock_conf.should_expose(state) + entity_conf["should_expose"] = True + assert mock_conf.should_expose(state) + + entity_conf["should_expose"] = None + assert mock_conf.should_expose(state) + + await cloud_prefs.async_update(google_default_expose=["sensor"],) + assert not mock_conf.should_expose(state) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index aa59e935a86..a3c33b31ebb 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -355,6 +355,8 @@ async def test_websocket_status( "google_enabled": True, "google_entity_configs": {}, "google_secure_devices_pin": None, + "google_default_expose": None, + "alexa_default_expose": None, "alexa_entity_configs": {}, "alexa_report_state": False, "google_report_state": False, @@ -487,6 +489,8 @@ async def test_websocket_update_preferences( "alexa_enabled": False, "google_enabled": False, "google_secure_devices_pin": "1234", + "google_default_expose": ["light", "switch"], + "alexa_default_expose": ["sensor", "media_player"], } ) response = await client.receive_json() @@ -495,6 +499,8 @@ async def test_websocket_update_preferences( assert not setup_api.google_enabled assert not setup_api.alexa_enabled assert setup_api.google_secure_devices_pin == "1234" + assert setup_api.google_default_expose == ["light", "switch"] + assert setup_api.alexa_default_expose == ["sensor", "media_player"] async def test_websocket_update_preferences_require_relink( @@ -746,6 +752,25 @@ async def test_update_google_entity(hass, hass_ws_client, setup_api, mock_cloud_ "disable_2fa": False, } + await client.send_json( + { + "id": 6, + "type": "cloud/google_assistant/entities/update", + "entity_id": "light.kitchen", + "should_expose": None, + } + ) + response = await client.receive_json() + + assert response["success"] + prefs = hass.data[DOMAIN].client.prefs + assert prefs.google_entity_configs["light.kitchen"] == { + "should_expose": None, + "override_name": "updated name", + "aliases": ["lefty", "righty"], + "disable_2fa": False, + } + async def test_enabling_remote_trusted_proxies_local4( hass, hass_ws_client, setup_api, mock_cloud_login @@ -834,6 +859,20 @@ async def test_update_alexa_entity(hass, hass_ws_client, setup_api, mock_cloud_l prefs = hass.data[DOMAIN].client.prefs assert prefs.alexa_entity_configs["light.kitchen"] == {"should_expose": False} + await client.send_json( + { + "id": 6, + "type": "cloud/alexa/entities/update", + "entity_id": "light.kitchen", + "should_expose": None, + } + ) + response = await client.receive_json() + + assert response["success"] + prefs = hass.data[DOMAIN].client.prefs + assert prefs.alexa_entity_configs["light.kitchen"] == {"should_expose": None} + async def test_sync_alexa_entities_timeout( hass, hass_ws_client, setup_api, mock_cloud_login From e6141ae55866a99809589272719d70699236c3fb Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Fri, 28 Aug 2020 10:02:12 -0500 Subject: [PATCH 404/862] Add description of what caused an automation trigger to fire (#39251) Co-authored-by: J. Nick Koston --- .../components/arcam_fmj/device_trigger.py | 9 ++++-- .../components/automation/__init__.py | 11 ++++++-- .../components/automation/logbook.py | 8 ++++-- .../components/geo_location/trigger.py | 28 +++++++++---------- .../homeassistant/triggers/event.py | 8 +++++- .../homeassistant/triggers/homeassistant.py | 17 +++++++++-- .../homeassistant/triggers/numeric_state.py | 1 + .../homeassistant/triggers/state.py | 1 + .../components/homeassistant/triggers/time.py | 16 +++++++---- .../homeassistant/triggers/time_pattern.py | 9 +++++- .../components/kodi/device_trigger.py | 6 +++- homeassistant/components/litejet/trigger.py | 1 + homeassistant/components/mqtt/trigger.py | 1 + homeassistant/components/sun/trigger.py | 13 ++++++++- homeassistant/components/template/trigger.py | 1 + homeassistant/components/webhook/trigger.py | 1 + homeassistant/components/zone/trigger.py | 12 +++++++- tests/components/automation/test_init.py | 11 ++++++-- 18 files changed, 119 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 549b4cf4f82..40feb63f9f3 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -59,11 +59,16 @@ async def async_attach_trigger( config = TRIGGER_SCHEMA(config) if config[CONF_TYPE] == "turn_on": + entity_id = config[CONF_ENTITY_ID] @callback def _handle_event(event: Event): - if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: - hass.async_run_job(action({"trigger": config}, context=event.context)) + if event.data[ATTR_ENTITY_ID] == entity_id: + hass.async_run_job( + action, + {"trigger": {**config, "description": f"{DOMAIN} - {entity_id}"}}, + event.context, + ) return hass.bus.async_listen(EVENT_TURN_ON, _handle_event) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 970653cd4df..5d4c4c43b06 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -81,6 +81,7 @@ EVENT_AUTOMATION_RELOADED = "automation_reloaded" EVENT_AUTOMATION_TRIGGERED = "automation_triggered" ATTR_LAST_TRIGGERED = "last_triggered" +ATTR_SOURCE = "source" ATTR_VARIABLES = "variables" SERVICE_TRIGGER = "trigger" @@ -396,10 +397,14 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self.async_set_context(trigger_context) self._last_triggered = utcnow() self.async_write_ha_state() + event_data = { + ATTR_NAME: self._name, + ATTR_ENTITY_ID: self.entity_id, + } + if "trigger" in variables and "description" in variables["trigger"]: + event_data[ATTR_SOURCE] = variables["trigger"]["description"] self.hass.bus.async_fire( - EVENT_AUTOMATION_TRIGGERED, - {ATTR_NAME: self._name, ATTR_ENTITY_ID: self.entity_id}, - context=trigger_context, + EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context ) self._logger.info("Executing %s", self._name) diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index 2e3ad2475fc..cc44296eaa6 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -2,7 +2,7 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import callback -from . import DOMAIN, EVENT_AUTOMATION_TRIGGERED +from . import ATTR_SOURCE, DOMAIN, EVENT_AUTOMATION_TRIGGERED @callback @@ -12,9 +12,13 @@ def async_describe_events(hass, async_describe_event): # type: ignore @callback def async_describe_logbook_event(event): # type: ignore """Describe a logbook event.""" + message = "has been triggered" + if ATTR_SOURCE in event.data: + message = f"{message} by {event.data[ATTR_SOURCE]}" return { "name": event.data.get(ATTR_NAME), - "message": "has been triggered", + "message": message, + "source": event.data.get(ATTR_SOURCE), "entity_id": event.data.get(ATTR_ENTITY_ID), } diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 92094a751a0..dc8879b51e5 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -67,20 +67,20 @@ async def async_attach_trigger(hass, config, action, automation_info): and not to_match ): hass.async_run_job( - action( - { - "trigger": { - "platform": "geo_location", - "source": source, - "entity_id": event.data.get("entity_id"), - "from_state": from_state, - "to_state": to_state, - "zone": zone_state, - "event": trigger_event, - } - }, - context=event.context, - ) + action, + { + "trigger": { + "platform": "geo_location", + "source": source, + "entity_id": event.data.get("entity_id"), + "from_state": from_state, + "to_state": to_state, + "zone": zone_state, + "event": trigger_event, + "description": f"geo_location - {source}", + } + }, + event.context, ) return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 2c247280e06..8fa9207c3b0 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -48,7 +48,13 @@ async def async_attach_trigger( hass.async_run_job( action, - {"trigger": {"platform": platform_type, "event": event}}, + { + "trigger": { + "platform": platform_type, + "event": event, + "description": f"event '{event.event_type}'", + } + }, event.context, ) diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 6ccb54d034e..baca145e19c 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -31,7 +31,13 @@ async def async_attach_trigger(hass, config, action, automation_info): """Execute when Home Assistant is shutting down.""" hass.async_run_job( action, - {"trigger": {"platform": "homeassistant", "event": event}}, + { + "trigger": { + "platform": "homeassistant", + "event": event, + "description": "Home Assistant stopping", + } + }, event.context, ) @@ -41,7 +47,14 @@ async def async_attach_trigger(hass, config, action, automation_info): # Check state because a config reload shouldn't trigger it. if automation_info["home_assistant_start"]: hass.async_run_job( - action({"trigger": {"platform": "homeassistant", "event": event}}) + action, + { + "trigger": { + "platform": "homeassistant", + "event": event, + "description": "Home Assistant starting", + } + }, ) return lambda: None diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index a1678e0a24c..b5a21aa8404 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -117,6 +117,7 @@ async def async_attach_trigger( "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity], + "description": f"numeric state of {entity}", } }, to_s.context, diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index d51e63964e9..c580b8daf49 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -103,6 +103,7 @@ async def async_attach_trigger( "to_state": to_s, "for": time_delta if not time_delta else period[entity], "attribute": attribute, + "description": f"state of {entity}", } }, event.context, diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 01eb1a9b85f..a152b1e8571 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -1,5 +1,6 @@ """Offer time listening automation rules.""" from datetime import datetime +from functools import partial import logging import voluptuous as vol @@ -38,9 +39,12 @@ async def async_attach_trigger(hass, config, action, automation_info): removes = [] @callback - def time_automation_listener(now): + def time_automation_listener(description, now): """Listen for time changes and calls action.""" - hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}}) + hass.async_run_job( + action, + {"trigger": {"platform": "time", "now": now, "description": description}}, + ) @callback def update_entity_trigger_event(event): @@ -81,13 +85,15 @@ async def async_attach_trigger(hass, config, action, automation_info): # Only set up listener if time is now or in the future. if trigger_dt >= dt_util.now(): remove = async_track_point_in_time( - hass, time_automation_listener, trigger_dt + hass, + partial(time_automation_listener, f"time set in {entity_id}"), + trigger_dt, ) elif has_time: # Else if it has time, then track time change. remove = async_track_time_change( hass, - time_automation_listener, + partial(time_automation_listener, f"time set in {entity_id}"), hour=hour, minute=minute, second=second, @@ -108,7 +114,7 @@ async def async_attach_trigger(hass, config, action, automation_info): removes.append( async_track_time_change( hass, - time_automation_listener, + partial(time_automation_listener, "time"), hour=at_time.hour, minute=at_time.minute, second=at_time.second, diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index 41294268ae8..adacc939870 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -75,7 +75,14 @@ async def async_attach_trigger(hass, config, action, automation_info): def time_automation_listener(now): """Listen for time changes and calls action.""" hass.async_run_job( - action, {"trigger": {"platform": "time_pattern", "now": now}} + action, + { + "trigger": { + "platform": "time_pattern", + "now": now, + "description": "time pattern", + } + }, ) return async_track_time_change( diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py index a5c93d08f72..85065a8bfe3 100644 --- a/homeassistant/components/kodi/device_trigger.py +++ b/homeassistant/components/kodi/device_trigger.py @@ -66,7 +66,11 @@ def _attach_trigger( @callback def _handle_event(event: Event): if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: - hass.async_run_job(action({"trigger": config}, context=event.context)) + hass.async_run_job( + action, + {"trigger": {**config, "description": event_type}}, + event.context, + ) return hass.bus.async_listen(event_type, _handle_event) diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index 5924cf3b809..d8b8ede8339 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -50,6 +50,7 @@ async def async_attach_trigger(hass, config, action, automation_info): CONF_NUMBER: number, CONF_HELD_MORE_THAN: held_more_than, CONF_HELD_LESS_THAN: held_less_than, + "description": f"litejet switch #{number}", } }, ) diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 8bb8ad46041..1ba7905120f 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -45,6 +45,7 @@ async def async_attach_trigger(hass, config, action, automation_info): "topic": mqttmsg.topic, "payload": mqttmsg.payload, "qos": mqttmsg.qos, + "description": f"mqtt topic {mqttmsg.topic}", } try: diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py index c416742f397..c21726cf0ae 100644 --- a/homeassistant/components/sun/trigger.py +++ b/homeassistant/components/sun/trigger.py @@ -31,12 +31,23 @@ async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) + description = event + if offset: + description = f"{description} with offset" @callback def call_action(): """Call action with right context.""" hass.async_run_job( - action, {"trigger": {"platform": "sun", "event": event, "offset": offset}} + action, + { + "trigger": { + "platform": "sun", + "event": event, + "offset": offset, + "description": description, + } + }, ) if event == SUN_EVENT_SUNRISE: diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index b6e6c974807..980faf4d0a8 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -62,6 +62,7 @@ async def async_attach_trigger( "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period, + "description": f"{entity_id} via template", } }, (to_s.context if to_s else None), diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 38ae9c5e364..cc03f74922f 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -30,6 +30,7 @@ async def _handle_webhook(action, hass, webhook_id, request): result["data"] = await request.post() result["query"] = request.query + result["description"] = "webhook" hass.async_run_job(action, {"trigger": result}) diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index f53af0e5d7f..1f0856513dd 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -1,7 +1,13 @@ """Offer zone automation rules.""" import voluptuous as vol -from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_PLATFORM, CONF_ZONE +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + CONF_ENTITY_ID, + CONF_EVENT, + CONF_PLATFORM, + CONF_ZONE, +) from homeassistant.core import callback from homeassistant.helpers import condition, config_validation as cv, location from homeassistant.helpers.event import async_track_state_change_event @@ -12,6 +18,8 @@ EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER +_EVENT_DESCRIPTION = {EVENT_ENTER: "entering", EVENT_LEAVE: "leaving"} + TRIGGER_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "zone", @@ -56,6 +64,7 @@ async def async_attach_trigger(hass, config, action, automation_info): and from_match and not to_match ): + description = f"{entity} {_EVENT_DESCRIPTION[event]} {zone_state.attributes[ATTR_FRIENDLY_NAME]}" hass.async_run_job( action, { @@ -66,6 +75,7 @@ async def async_attach_trigger(hass, config, action, automation_info): "to_state": to_s, "zone": zone_state, "event": event, + "description": description, } }, to_s.context, diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index d1c2e47a39e..b8f25dc9c46 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -6,6 +6,7 @@ import pytest from homeassistant.components import logbook import homeassistant.components.automation as automation from homeassistant.components.automation import ( + ATTR_SOURCE, DOMAIN, EVENT_AUTOMATION_RELOADED, EVENT_AUTOMATION_TRIGGERED, @@ -324,6 +325,7 @@ async def test_shared_context(hass, calls): # Ensure event data has all attributes set assert args[0].data.get(ATTR_NAME) is not None assert args[0].data.get(ATTR_ENTITY_ID) is not None + assert args[0].data.get(ATTR_SOURCE) is not None # Ensure context set correctly for event fired by 'hello' automation args, _ = first_automation_listener.call_args @@ -341,6 +343,7 @@ async def test_shared_context(hass, calls): # Ensure event data has all attributes set assert args[0].data.get(ATTR_NAME) is not None assert args[0].data.get(ATTR_ENTITY_ID) is not None + assert args[0].data.get(ATTR_SOURCE) is not None # Ensure the service call from the second automation # shares the same context @@ -1089,7 +1092,11 @@ async def test_logbook_humanify_automation_triggered_event(hass): ), MockLazyEventPartialState( EVENT_AUTOMATION_TRIGGERED, - {ATTR_ENTITY_ID: "automation.bye", ATTR_NAME: "Bye Automation"}, + { + ATTR_ENTITY_ID: "automation.bye", + ATTR_NAME: "Bye Automation", + ATTR_SOURCE: "source of trigger", + }, ), ], entity_attr_cache, @@ -1104,5 +1111,5 @@ async def test_logbook_humanify_automation_triggered_event(hass): assert event2["name"] == "Bye Automation" assert event2["domain"] == "automation" - assert event2["message"] == "has been triggered" + assert event2["message"] == "has been triggered by source of trigger" assert event2["entity_id"] == "automation.bye" From d2195e2b373117bc624fe0ce67289342ce2b56fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 10:23:19 -0500 Subject: [PATCH 405/862] Add support for reloading min_max from yaml (#39327) * Add support for reloading min_max from yaml * git add --- homeassistant/components/min_max/__init__.py | 3 + homeassistant/components/min_max/sensor.py | 11 +++- .../components/min_max/services.yaml | 2 + tests/components/min_max/test_sensor.py | 55 ++++++++++++++++++- tests/fixtures/min_max/configuration.yaml | 7 +++ 5 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/min_max/services.yaml create mode 100644 tests/fixtures/min_max/configuration.yaml diff --git a/homeassistant/components/min_max/__init__.py b/homeassistant/components/min_max/__init__.py index d13d963ff47..4baf96471aa 100644 --- a/homeassistant/components/min_max/__init__.py +++ b/homeassistant/components/min_max/__init__.py @@ -1 +1,4 @@ """The min_max component.""" + +DOMAIN = "min_max" +PLATFORMS = ["sensor"] diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index a5e31829d97..a54fa3e2d46 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -15,6 +15,9 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.reload import async_setup_reload_service + +from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -72,6 +75,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensor_type = config.get(CONF_TYPE) round_digits = config.get(CONF_ROUND_DIGITS) + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + async_add_entities( [MinMaxSensor(hass, entity_ids, name, sensor_type, round_digits)], True ) @@ -188,8 +193,10 @@ class MinMaxSensor(Entity): hass.async_add_job(self.async_update_ha_state, True) - async_track_state_change_event( - hass, entity_ids, async_min_max_sensor_state_listener + self.async_on_remove( + async_track_state_change_event( + hass, entity_ids, async_min_max_sensor_state_listener + ) ) @property diff --git a/homeassistant/components/min_max/services.yaml b/homeassistant/components/min_max/services.yaml new file mode 100644 index 00000000000..c91b36249ac --- /dev/null +++ b/homeassistant/components/min_max/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all min_max entities. diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index bece386a89e..8f532a6ec07 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -1,16 +1,22 @@ """The test for the min/max sensor platform.""" +from os import path import statistics import unittest +from asynctest.mock import patch + +from homeassistant import config as hass_config +from homeassistant.components.min_max import DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE, ) -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant @@ -332,3 +338,50 @@ class TestMinMaxSensor(unittest.TestCase): assert self.max == state.attributes.get("max_value") assert self.mean == state.attributes.get("mean") assert self.median == state.attributes.get("median") + + +async def test_reload(hass): + """Verify we can reload filter sensors.""" + hass.states.async_set("sensor.test_1", 12345) + hass.states.async_set("sensor.test_2", 45678) + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "min_max", + "name": "test", + "type": "mean", + "entity_ids": ["sensor.test_1", "sensor.test_2"], + } + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.test") + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "min_max/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.test") is None + assert hass.states.get("sensor.second_test") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/min_max/configuration.yaml b/tests/fixtures/min_max/configuration.yaml new file mode 100644 index 00000000000..707b57ffab4 --- /dev/null +++ b/tests/fixtures/min_max/configuration.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: min_max + entity_ids: + - sensor.test_1 + - sensor.test_2 + name: second_test + type: mean From 4b5d0915a9e1bd72434ef4013eaa9eb4d6f3f17e Mon Sep 17 00:00:00 2001 From: Jin Date: Fri, 28 Aug 2020 23:24:14 +0800 Subject: [PATCH 406/862] Add support for hmi208(xiaomi plug BLE) (#39306) As this plug has already been supported in [python-miio v5.1](https://github.com/rytilahti/python-miio/releases/tag/0.5.1). Changes towards xiaomi-miio made for fully supporting this device in HA. --- homeassistant/components/xiaomi_miio/switch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 26ffb7e578f..6dfcc539a44 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -57,6 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( "chuangmi.plug.v3", "chuangmi.plug.hmi205", "chuangmi.plug.hmi206", + "chuangmi.plug.hmi208", "lumi.acpartner.v3", ] ), @@ -142,7 +143,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= except DeviceException as ex: raise PlatformNotReady from ex - if model in ["chuangmi.plug.v1", "chuangmi.plug.v3"]: + if model in ["chuangmi.plug.v1", "chuangmi.plug.v3", "chuangmi.plug.hmi208"]: plug = ChuangmiPlug(host, token, model=model) # The device has two switchable channels (mains and a USB port). From d9f3bdea53e93836e73a19acd9e5e6feaf4c7867 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Aug 2020 15:33:08 +0000 Subject: [PATCH 407/862] Black --- tests/components/cloud/test_alexa_config.py | 4 +++- tests/components/cloud/test_google_config.py | 4 +++- tests/components/generic/test_camera.py | 9 +++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index cbeda41dac7..e54a5dcde01 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -25,7 +25,9 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): entity_conf["should_expose"] = None assert conf.should_expose("light.kitchen") - await cloud_prefs.async_update(alexa_default_expose=["sensor"],) + await cloud_prefs.async_update( + alexa_default_expose=["sensor"], + ) assert not conf.should_expose("light.kitchen") diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 977df95051e..78207605830 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -187,5 +187,7 @@ async def test_google_config_expose_entity_prefs(mock_conf, cloud_prefs): entity_conf["should_expose"] = None assert mock_conf.should_expose(state) - await cloud_prefs.async_update(google_default_expose=["sensor"],) + await cloud_prefs.async_update( + google_default_expose=["sensor"], + ) assert not mock_conf.should_expose(state) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index c3400d8f57c..7be1670dd4c 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -330,11 +330,16 @@ async def test_reloading(aioclient_mock, hass, hass_client): assert body == "hello world" yaml_path = path.join( - _get_fixtures_base_path(), "fixtures", "generic/configuration.yaml", + _get_fixtures_base_path(), + "fixtures", + "generic/configuration.yaml", ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - DOMAIN, SERVICE_RELOAD, {}, blocking=True, + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, ) await hass.async_block_till_done() From 4b8217777e596b1b0b6112ce70e46bdaca0813f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Aug 2020 17:33:34 +0200 Subject: [PATCH 408/862] Add basic light and sensor support to Shelly (#39288) * Add basic light platform * Add sensor support * Bump aioshelly to 0.2.1 * Lint * Use UNIT_PERCENTAGE Co-authored-by: Maciej Bieniek * Format sensor.py Co-authored-by: Maciej Bieniek --- .coveragerc | 2 + homeassistant/components/shelly/__init__.py | 7 +- .../components/shelly/config_flow.py | 4 +- homeassistant/components/shelly/light.py | 74 +++++++++++++++++ homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/sensor.py | 83 +++++++++++++++++++ homeassistant/components/shelly/switch.py | 21 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 181 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/shelly/light.py create mode 100644 homeassistant/components/shelly/sensor.py diff --git a/.coveragerc b/.coveragerc index 72f5ffa5812..663031c1589 100644 --- a/.coveragerc +++ b/.coveragerc @@ -754,6 +754,8 @@ omit = homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py + homeassistant/components/shelly/light.py + homeassistant/components/shelly/sensor.py homeassistant/components/shelly/switch.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index f9b936898a1..093790644bb 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers import ( from .const import DOMAIN -PLATFORMS = ["switch"] +PLATFORMS = ["switch", "light", "sensor"] _LOGGER = logging.getLogger(__name__) @@ -129,11 +129,12 @@ class ShellyBlockEntity(entity.Entity): """Initialize Shelly entity.""" self.wrapper = wrapper self.block = block + self._name = f"{self.wrapper.name} - {self.block.description.replace('_', ' ')}" @property def name(self): """Name of entity.""" - return f"{self.wrapper.name} - {self.block.description}" + return self._name @property def should_poll(self): @@ -155,7 +156,7 @@ class ShellyBlockEntity(entity.Entity): @property def unique_id(self): """Return unique ID of entity.""" - return f"{self.wrapper.mac}-{self.block.index}" + return f"{self.wrapper.mac}-{self.block.description}" async def async_added_to_hass(self): """When entity is added to HASS.""" diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 830cb81c74e..c464f0d7adb 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -61,7 +61,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: device_info = await validate_input(self.hass, user_input) - except asyncio.TimeoutError: + except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") @@ -103,7 +103,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: device_info = await validate_input(self.hass, {"host": self.host}) - except asyncio.TimeoutError: + except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py new file mode 100644 index 00000000000..9e9b9e350a0 --- /dev/null +++ b/homeassistant/components/shelly/light.py @@ -0,0 +1,74 @@ +"""Light for Shelly.""" +from aioshelly import Block + +from homeassistant.components.light import SUPPORT_BRIGHTNESS, LightEntity +from homeassistant.core import callback + +from . import ShellyBlockEntity, ShellyDeviceWrapper +from .const import DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up lights for device.""" + wrapper = hass.data[DOMAIN][config_entry.entry_id] + blocks = [block for block in wrapper.device.blocks if block.type == "light"] + + if not blocks: + return + + async_add_entities(ShellyLight(wrapper, block) for block in blocks) + + +class ShellyLight(ShellyBlockEntity, LightEntity): + """Switch that controls a relay block on Shelly devices.""" + + def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: + """Initialize light.""" + super().__init__(wrapper, block) + self.control_result = None + self._supported_features = 0 + if hasattr(block, "brightness"): + self._supported_features |= SUPPORT_BRIGHTNESS + + @property + def is_on(self) -> bool: + """If light is on.""" + if self.control_result: + return self.control_result["ison"] + + return self.block.output + + @property + def brightness(self): + """Brightness of light.""" + if self.control_result: + brightness = self.control_result["brightness"] + else: + brightness = self.block.brightness + return int(brightness / 100 * 255) + + @property + def supported_features(self): + """Supported features.""" + return self._supported_features + + async def async_turn_on( + self, brightness=None, **kwargs + ): # pylint: disable=arguments-differ + """Turn on light.""" + params = {"turn": "on"} + if brightness is not None: + params["brightness"] = int(brightness / 255 * 100) + self.control_result = await self.block.set_state(**params) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn off light.""" + self.control_result = await self.block.set_state(turn="off") + self.async_write_ha_state() + + @callback + def _update_callback(self): + """When device updates, clear control result that overrides state.""" + self.control_result = None + super()._update_callback() diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 149b8ef18cd..4f4e740a83f 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly2", - "requirements": ["aioshelly==0.1.2"], + "requirements": ["aioshelly==0.2.1"], "zeroconf": ["_http._tcp.local."], "codeowners": ["@balloob"] } diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py new file mode 100644 index 00000000000..2af6d1d91a9 --- /dev/null +++ b/homeassistant/components/shelly/sensor.py @@ -0,0 +1,83 @@ +"""Sensor for Shelly.""" +import aioshelly + +from homeassistant.components import sensor +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE +from homeassistant.helpers.entity import Entity + +from . import ShellyBlockEntity, ShellyDeviceWrapper +from .const import DOMAIN + +SENSORS = { + "extTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], + "humidity": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_HUMIDITY], +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors for device.""" + wrapper = hass.data[DOMAIN][config_entry.entry_id] + sensors = [] + + for block in wrapper.device.blocks: + if block.type != "sensor": + continue + + for attr in SENSORS: + if not hasattr(block, attr): + continue + + sensors.append(ShellySensor(wrapper, block, attr)) + + if sensors: + async_add_entities(sensors) + + +class ShellySensor(ShellyBlockEntity, Entity): + """Switch that controls a relay block on Shelly devices.""" + + def __init__( + self, + wrapper: ShellyDeviceWrapper, + block: aioshelly.Block, + attribute: str, + ) -> None: + """Initialize sensor.""" + super().__init__(wrapper, block) + self.attribute = attribute + unit, device_class = SENSORS[attribute] + info = block.info(attribute) + + if info[aioshelly.BLOCK_VALUE_TYPE] == aioshelly.BLOCK_VALUE_TYPE_TEMPERATURE: + if info[aioshelly.BLOCK_VALUE_UNIT] == "C": + unit = TEMP_CELSIUS + else: + unit = TEMP_FAHRENHEIT + + self._unit = unit + self._device_class = device_class + + @property + def unique_id(self): + """Return unique ID of entity.""" + return f"{super().unique_id}-{self.attribute}" + + @property + def name(self): + """Name of sensor.""" + return f"{self.wrapper.name} - {self.attribute}" + + @property + def state(self): + """Value of sensor.""" + return getattr(self.block, self.attribute) + + @property + def unit_of_measurement(self): + """Return unit of sensor.""" + return self._unit + + @property + def device_class(self): + """Device class of sensor.""" + return self._device_class diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 4a6c2a21b0b..9bd14c49ab3 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -1,22 +1,25 @@ """Switch for Shelly.""" -from homeassistant.components.shelly import ShellyBlockEntity +from aioshelly import RelayBlock + from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback +from . import ShellyBlockEntity, ShellyDeviceWrapper from .const import DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up switches for device.""" wrapper = hass.data[DOMAIN][config_entry.entry_id] + + if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": + return + relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"] if not relay_blocks: return - if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": - return - multiple_blocks = len(relay_blocks) > 1 async_add_entities( RelaySwitch(wrapper, block, multiple_blocks=multiple_blocks) @@ -27,9 +30,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RelaySwitch(ShellyBlockEntity, SwitchEntity): """Switch that controls a relay block on Shelly devices.""" - def __init__(self, *args, multiple_blocks) -> None: + def __init__( + self, wrapper: ShellyDeviceWrapper, block: RelayBlock, multiple_blocks + ) -> None: """Initialize relay switch.""" - super().__init__(*args) + super().__init__(wrapper, block) self.multiple_blocks = multiple_blocks self.control_result = None @@ -56,12 +61,12 @@ class RelaySwitch(ShellyBlockEntity, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn on relay.""" - self.control_result = await self.block.turn_on() + self.control_result = await self.block.set_state(turn="on") self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn off relay.""" - self.control_result = await self.block.turn_off() + self.control_result = await self.block.set_state(turn="off") self.async_write_ha_state() @callback diff --git a/requirements_all.txt b/requirements_all.txt index bb7f1aa0960..65d2e4cc161 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.1.2 +aioshelly==0.2.1 # homeassistant.components.switcher_kis aioswitcher==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c402aabd39e..2ea965c28b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.1.2 +aioshelly==0.2.1 # homeassistant.components.switcher_kis aioswitcher==1.2.0 From 3377f6b12a7a4c0d52d3d8784cabbb22a11805f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 12:18:02 -0500 Subject: [PATCH 409/862] Register mobile_app notification services when a new device is added (#39356) * Register mobile_app notification services when a new device is added * targets and base service use their own patterns to generate the name --- .../components/mobile_app/__init__.py | 4 +- homeassistant/components/notify/__init__.py | 148 ++++++++++++------ tests/components/mobile_app/test_notify.py | 29 ++++ 3 files changed, 129 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 4396f8c8e0c..3f5b42259ee 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,7 +1,7 @@ """Integrates Native Apps to Home Assistant.""" import asyncio -from homeassistant.components import cloud +from homeassistant.components import cloud, notify as hass_notify from homeassistant.components.webhook import ( async_register as webhook_register, async_unregister as webhook_unregister, @@ -101,6 +101,8 @@ async def async_setup_entry(hass, entry): hass.config_entries.async_forward_entry_setup(entry, domain) ) + await hass_notify.async_reload(hass, DOMAIN) + return True diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 1ea0b9aa6d5..8d7a86d17ee 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -11,6 +11,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import bind_hass from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import slugify @@ -35,6 +36,12 @@ DOMAIN = "notify" SERVICE_NOTIFY = "notify" +NOTIFY_SERVICES = "notify_services" +SERVICE = "service" +TARGETS = "targets" +FRIENDLY_NAME = "friendly_name" +TARGET_FRIENDLY_NAME = "target_friendly_name" + PLATFORM_SCHEMA = vol.Schema( {vol.Required(CONF_PLATFORM): cv.string, vol.Optional(CONF_NAME): cv.string}, extra=vol.ALLOW_EXTRA, @@ -50,22 +57,89 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema( ) +@bind_hass +async def async_reload(hass, integration_name): + """Register notify services for an integration.""" + if NOTIFY_SERVICES not in hass.data: + return + + data = hass.data[NOTIFY_SERVICES][integration_name] + notify_service = data[SERVICE] + friendly_name = data[FRIENDLY_NAME] + targets = data[TARGETS] + + async def _async_notify_message(service): + """Handle sending notification message service calls.""" + await _async_notify_message_service(hass, service, notify_service, targets) + + if hasattr(notify_service, "targets"): + target_friendly_name = data[TARGET_FRIENDLY_NAME] + for name, target in notify_service.targets.items(): + target_name = slugify(f"{target_friendly_name}_{name}") + if target_name in targets: + continue + targets[target_name] = target + hass.services.async_register( + DOMAIN, + target_name, + _async_notify_message, + schema=NOTIFY_SERVICE_SCHEMA, + ) + + friendly_name_slug = slugify(friendly_name) + if hass.services.has_service(DOMAIN, friendly_name_slug): + return + + hass.services.async_register( + DOMAIN, + friendly_name_slug, + _async_notify_message, + schema=NOTIFY_SERVICE_SCHEMA, + ) + + +async def _async_notify_message_service(hass, service, notify_service, targets): + """Handle sending notification message service calls.""" + kwargs = {} + message = service.data[ATTR_MESSAGE] + title = service.data.get(ATTR_TITLE) + + if title: + title.hass = hass + kwargs[ATTR_TITLE] = title.async_render() + + if targets.get(service.service) is not None: + kwargs[ATTR_TARGET] = [targets[service.service]] + elif service.data.get(ATTR_TARGET) is not None: + kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) + + message.hass = hass + kwargs[ATTR_MESSAGE] = message.async_render() + kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) + + await notify_service.async_send_message(**kwargs) + + async def async_setup(hass, config): """Set up the notify services.""" - targets = {} + hass.data.setdefault(NOTIFY_SERVICES, {}) - async def async_setup_platform(p_type, p_config=None, discovery_info=None): + async def async_setup_platform( + integration_name, p_config=None, discovery_info=None + ): """Set up a notify platform.""" if p_config is None: p_config = {} - platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) + platform = await async_prepare_setup_platform( + hass, config, DOMAIN, integration_name + ) if platform is None: _LOGGER.error("Unknown notification service specified") return - _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) + _LOGGER.info("Setting up %s.%s", DOMAIN, integration_name) notify_service = None try: if hasattr(platform, "async_get_service"): @@ -84,12 +158,12 @@ async def async_setup(hass, config): # on discovery data. if discovery_info is None: _LOGGER.error( - "Failed to initialize notification service %s", p_type + "Failed to initialize notification service %s", integration_name ) return except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error setting up platform %s", p_type) + _LOGGER.exception("Error setting up platform %s", integration_name) return notify_service.hass = hass @@ -97,60 +171,32 @@ async def async_setup(hass, config): if discovery_info is None: discovery_info = {} - async def async_notify_message(service): - """Handle sending notification message service calls.""" - kwargs = {} - message = service.data[ATTR_MESSAGE] - title = service.data.get(ATTR_TITLE) + target_friendly_name = ( + p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or integration_name + ) - if title: - title.hass = hass - kwargs[ATTR_TITLE] = title.async_render() - - if targets.get(service.service) is not None: - kwargs[ATTR_TARGET] = [targets[service.service]] - elif service.data.get(ATTR_TARGET) is not None: - kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) - - message.hass = hass - kwargs[ATTR_MESSAGE] = message.async_render() - kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) - - await notify_service.async_send_message(**kwargs) - - if hasattr(notify_service, "targets"): - platform_name = ( - p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or p_type - ) - for name, target in notify_service.targets.items(): - target_name = slugify(f"{platform_name}_{name}") - targets[target_name] = target - hass.services.async_register( - DOMAIN, - target_name, - async_notify_message, - schema=NOTIFY_SERVICE_SCHEMA, - ) - - platform_name = ( + friendly_name = ( p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or SERVICE_NOTIFY ) - platform_name_slug = slugify(platform_name) - hass.services.async_register( - DOMAIN, - platform_name_slug, - async_notify_message, - schema=NOTIFY_SERVICE_SCHEMA, - ) + hass.data[NOTIFY_SERVICES][integration_name] = { + FRIENDLY_NAME: friendly_name, + # The targets use a slightly different friendly name + # selection pattern than the base service + TARGET_FRIENDLY_NAME: target_friendly_name, + SERVICE: notify_service, + TARGETS: {}, + } - hass.config.components.add(f"{DOMAIN}.{p_type}") + await async_reload(hass, integration_name) + + hass.config.components.add(f"{DOMAIN}.{integration_name}") return True setup_tasks = [ - async_setup_platform(p_type, p_config) - for p_type, p_config in config_per_platform(config, DOMAIN) + async_setup_platform(integration_name, p_config) + for integration_name, p_config in config_per_platform(config, DOMAIN) ] if setup_tasks: diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index 860f3d9f81f..a52477c6642 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -61,6 +61,35 @@ async def setup_push_receiver(hass, aioclient_mock): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() + loaded_late_entry = MockConfigEntry( + connection_class="cloud_push", + data={ + "app_data": {"push_token": "PUSH_TOKEN2", "push_url": f"{push_url}2"}, + "app_id": "io.homeassistant.mobile_app", + "app_name": "mobile_app tests", + "app_version": "1.0", + "device_id": "4d5e6f2", + "device_name": "Loaded Late", + "manufacturer": "Home Assistant", + "model": "mobile_app", + "os_name": "Linux", + "os_version": "5.0.6", + "secret": "123abc2", + "supports_encryption": False, + "user_id": "1a2b3c2", + "webhook_id": "webhook_id_2", + }, + domain=DOMAIN, + source="registration", + title="mobile_app 2 test entry", + version=1, + ) + loaded_late_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(loaded_late_entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service("notify", "mobile_app_loaded_late") + async def test_notify_works(hass, aioclient_mock, setup_push_receiver): """Test notify works.""" From 33a05541a499db706ab567dce9612f3cfd443145 Mon Sep 17 00:00:00 2001 From: Anna Tikhomirova Date: Fri, 28 Aug 2020 20:36:59 +0300 Subject: [PATCH 410/862] Simplify mobile app debugging by adding sender device name (#38518) * Simplify mobile app debugging by adding sender device name. * Reformatted webhook.py with black --- homeassistant/components/mobile_app/webhook.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 01db11a04e3..9d614664a62 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -184,8 +184,13 @@ async def handle_webhook( _LOGGER.error("Received invalid webhook type: %s", webhook_type) return empty_okay_response() + device_name = config_entry.data[ATTR_DEVICE_NAME] + _LOGGER.debug( - "Received webhook payload for type %s: %s", webhook_type, webhook_payload + "Received webhook payload from %s for type %s: %s", + device_name, + webhook_type, + webhook_payload, ) # Shield so we make sure we finish the webhook, even if sender hangs up. From 85869be2d8fcddf12bbd5ed88f7b060055628d04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 12:37:19 -0500 Subject: [PATCH 411/862] Unregister mobile_app notification services when a device is removed (#39359) --- homeassistant/components/mobile_app/__init__.py | 6 +++++- homeassistant/components/notify/__init__.py | 15 ++++++++++++++- tests/components/mobile_app/test_notify.py | 6 ++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 3f5b42259ee..264017796aa 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -119,7 +119,11 @@ async def async_unload_entry(hass, entry): if not unload_ok: return False - webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + webhook_id = entry.data[CONF_WEBHOOK_ID] + + webhook_unregister(hass, webhook_id) + del hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] + await hass_notify.async_reload(hass, DOMAIN) return True diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 8d7a86d17ee..a68d6ded5c8 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -60,7 +60,10 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema( @bind_hass async def async_reload(hass, integration_name): """Register notify services for an integration.""" - if NOTIFY_SERVICES not in hass.data: + if ( + NOTIFY_SERVICES not in hass.data + or integration_name not in hass.data[NOTIFY_SERVICES] + ): return data = hass.data[NOTIFY_SERVICES][integration_name] @@ -74,8 +77,12 @@ async def async_reload(hass, integration_name): if hasattr(notify_service, "targets"): target_friendly_name = data[TARGET_FRIENDLY_NAME] + stale_targets = set(targets) + for name, target in notify_service.targets.items(): target_name = slugify(f"{target_friendly_name}_{name}") + if target_name in stale_targets: + stale_targets.remove(target_name) if target_name in targets: continue targets[target_name] = target @@ -86,6 +93,12 @@ async def async_reload(hass, integration_name): schema=NOTIFY_SERVICE_SCHEMA, ) + for stale_target_name in stale_targets: + hass.services.async_remove( + DOMAIN, + stale_target_name, + ) + friendly_name_slug = slugify(friendly_name) if hass.services.has_service(DOMAIN, friendly_name_slug): return diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index a52477c6642..e1320a1f4f7 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -90,6 +90,12 @@ async def setup_push_receiver(hass, aioclient_mock): assert hass.services.has_service("notify", "mobile_app_loaded_late") + assert await hass.config_entries.async_remove(loaded_late_entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service("notify", "mobile_app_test") + assert not hass.services.has_service("notify", "mobile_app_loaded_late") + async def test_notify_works(hass, aioclient_mock, setup_push_receiver): """Test notify works.""" From 57848cdf35256ce055d94987a548d16b37f8272e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 12:40:30 -0500 Subject: [PATCH 412/862] Add the ability to reload ping platforms from yaml (#39344) --- homeassistant/components/ping/__init__.py | 3 ++ .../components/ping/binary_sensor.py | 4 ++ homeassistant/components/ping/services.yaml | 2 + requirements_test_all.txt | 3 ++ tests/components/ping/__init__.py | 1 + tests/components/ping/test_binary_sensor.py | 53 +++++++++++++++++++ tests/fixtures/ping/configuration.yaml | 5 ++ 7 files changed, 71 insertions(+) create mode 100644 homeassistant/components/ping/services.yaml create mode 100644 tests/components/ping/__init__.py create mode 100644 tests/components/ping/test_binary_sensor.py create mode 100644 tests/fixtures/ping/configuration.yaml diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index e55f13dc717..d5ec35276cf 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -1 +1,4 @@ """The ping component.""" + +DOMAIN = "ping" +PLATFORMS = ["binary_sensor"] diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 535c5bcd6a2..cb0d025f65d 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -12,7 +12,9 @@ import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service +from . import DOMAIN, PLATFORMS from .const import PING_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -56,6 +58,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None) -> None: """Set up the Ping Binary sensor.""" + setup_reload_service(hass, DOMAIN, PLATFORMS) + host = config[CONF_HOST] count = config[CONF_PING_COUNT] name = config.get(CONF_NAME, f"{DEFAULT_NAME} {host}") diff --git a/homeassistant/components/ping/services.yaml b/homeassistant/components/ping/services.yaml new file mode 100644 index 00000000000..e2da0c28627 --- /dev/null +++ b/homeassistant/components/ping/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all ping entities. diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ea965c28b6..511776e5f21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -391,6 +391,9 @@ huawei-lte-api==1.4.12 # homeassistant.components.iaqualink iaqualink==0.3.4 +# homeassistant.components.ping +icmplib==1.1.1 + # homeassistant.components.influxdb influxdb-client==1.8.0 diff --git a/tests/components/ping/__init__.py b/tests/components/ping/__init__.py new file mode 100644 index 00000000000..3d695fea171 --- /dev/null +++ b/tests/components/ping/__init__.py @@ -0,0 +1 @@ +"""Tests for ping component.""" diff --git a/tests/components/ping/test_binary_sensor.py b/tests/components/ping/test_binary_sensor.py new file mode 100644 index 00000000000..ae6362939a7 --- /dev/null +++ b/tests/components/ping/test_binary_sensor.py @@ -0,0 +1,53 @@ +"""The test for the ping binary_sensor platform.""" +from os import path + +from homeassistant import config as hass_config, setup +from homeassistant.components.ping import DOMAIN +from homeassistant.const import SERVICE_RELOAD + +from tests.async_mock import patch + + +async def test_reload(hass): + """Verify we can reload trend sensors.""" + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "ping", + "name": "test", + "host": "127.0.0.1", + "count": 1, + } + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("binary_sensor.test") + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "ping/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("binary_sensor.test") is None + assert hass.states.get("binary_sensor.test2") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/ping/configuration.yaml b/tests/fixtures/ping/configuration.yaml new file mode 100644 index 00000000000..201c020835e --- /dev/null +++ b/tests/fixtures/ping/configuration.yaml @@ -0,0 +1,5 @@ +binary_sensor: + - platform: ping + name: test2 + host: 127.0.0.1 + count: 1 From a99efcb5c2ee4f31c73e23f8133b3cc46d7dffa1 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Fri, 28 Aug 2020 13:09:43 -0500 Subject: [PATCH 413/862] Fix sun integration vulnerability to sudden large clock changes (#39335) * Fix sun integration vulnerability to sudden large clock changes * Fix update_sun_position as well --- homeassistant/components/sun/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index fe89413f4d5..5fb777bd325 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -104,7 +104,7 @@ class Sun(Entity): if location == self.location: return self.location = location - self.update_events(dt_util.utcnow()) + self.update_events() update_location(None) self.hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_location) @@ -148,8 +148,10 @@ class Sun(Entity): return next_utc @callback - def update_events(self, utc_point_in_time): + def update_events(self, now=None): """Update the attributes containing solar events.""" + # Grab current time in case system clock changed since last time we ran. + utc_point_in_time = dt_util.utcnow() self._next_change = utc_point_in_time + timedelta(days=400) # Work our way around the solar cycle, figure out the next @@ -207,7 +209,7 @@ class Sun(Entity): _LOGGER.debug( "sun phase_update@%s: phase=%s", utc_point_in_time.isoformat(), self.phase ) - self.update_sun_position(utc_point_in_time) + self.update_sun_position() # Set timer for the next solar event event.async_track_point_in_utc_time( @@ -216,8 +218,10 @@ class Sun(Entity): _LOGGER.debug("next time: %s", self._next_change.isoformat()) @callback - def update_sun_position(self, utc_point_in_time): + def update_sun_position(self, now=None): """Calculate the position of the sun.""" + # Grab current time in case system clock changed since last time we ran. + utc_point_in_time = dt_util.utcnow() self.solar_azimuth = round(self.location.solar_azimuth(utc_point_in_time), 2) self.solar_elevation = round( self.location.solar_elevation(utc_point_in_time), 2 From 92c06f0818d813aac4a9fa2b606237b66eeb0960 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 14:08:09 -0500 Subject: [PATCH 414/862] Ensure mobile_app notifications get re-registered after adding,removing,adding (#39362) --- homeassistant/components/notify/__init__.py | 15 +++++++++++---- tests/components/mobile_app/test_notify.py | 7 +++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index a68d6ded5c8..aa6de463e8c 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -66,7 +66,12 @@ async def async_reload(hass, integration_name): ): return - data = hass.data[NOTIFY_SERVICES][integration_name] + for data in hass.data[NOTIFY_SERVICES][integration_name]: + await _async_setup_notify_services(hass, data) + + +async def _async_setup_notify_services(hass, data): + """Create or remove the notify services.""" notify_service = data[SERVICE] friendly_name = data[FRIENDLY_NAME] targets = data[TARGETS] @@ -94,6 +99,7 @@ async def async_reload(hass, integration_name): ) for stale_target_name in stale_targets: + del targets[stale_target_name] hass.services.async_remove( DOMAIN, stale_target_name, @@ -187,12 +193,11 @@ async def async_setup(hass, config): target_friendly_name = ( p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or integration_name ) - friendly_name = ( p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or SERVICE_NOTIFY ) - hass.data[NOTIFY_SERVICES][integration_name] = { + data = { FRIENDLY_NAME: friendly_name, # The targets use a slightly different friendly name # selection pattern than the base service @@ -200,8 +205,10 @@ async def async_setup(hass, config): SERVICE: notify_service, TARGETS: {}, } + hass.data[NOTIFY_SERVICES].setdefault(integration_name, []) + hass.data[NOTIFY_SERVICES][integration_name].append(data) - await async_reload(hass, integration_name) + await _async_setup_notify_services(hass, data) hass.config.components.add(f"{DOMAIN}.{integration_name}") diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index e1320a1f4f7..5041b2453d9 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -96,6 +96,13 @@ async def setup_push_receiver(hass, aioclient_mock): assert hass.services.has_service("notify", "mobile_app_test") assert not hass.services.has_service("notify", "mobile_app_loaded_late") + loaded_late_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(loaded_late_entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service("notify", "mobile_app_test") + assert hass.services.has_service("notify", "mobile_app_loaded_late") + async def test_notify_works(hass, aioclient_mock, setup_push_receiver): """Test notify works.""" From b315df211877f3902febfd8588ce5d83feb1f23a Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Fri, 28 Aug 2020 14:51:15 -0500 Subject: [PATCH 415/862] Reduce automation state changes by using script helper's last_triggered attribute (#39323) --- .../components/automation/__init__.py | 22 +++++++++---------- homeassistant/helpers/script.py | 11 ++++++---- tests/components/automation/test_init.py | 8 ++----- tests/helpers/test_script.py | 19 ++++++++++++++++ 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 5d4c4c43b06..746ab3adc03 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -47,7 +47,7 @@ from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass -from homeassistant.util.dt import parse_datetime, utcnow +from homeassistant.util.dt import parse_datetime # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -247,7 +247,6 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._cond_func = cond_func self.action_script = action_script self.action_script.change_listener = self.async_write_ha_state - self._last_triggered = None self._initial_state = initial_state self._is_enabled = False self._referenced_entities: Optional[Set[str]] = None @@ -273,7 +272,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): def state_attributes(self): """Return the entity state attributes.""" attrs = { - ATTR_LAST_TRIGGERED: self._last_triggered, + ATTR_LAST_TRIGGERED: self.action_script.last_triggered, ATTR_MODE: self.action_script.script_mode, ATTR_CUR: self.action_script.runs, } @@ -339,7 +338,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): enable_automation = state.state == STATE_ON last_triggered = state.attributes.get("last_triggered") if last_triggered is not None: - self._last_triggered = parse_datetime(last_triggered) + self.action_script.last_triggered = parse_datetime(last_triggered) self._logger.debug( "Loaded automation %s with state %s from state " " storage last state %s", @@ -395,22 +394,23 @@ class AutomationEntity(ToggleEntity, RestoreEntity): trigger_context = Context(parent_id=parent_id) self.async_set_context(trigger_context) - self._last_triggered = utcnow() - self.async_write_ha_state() event_data = { ATTR_NAME: self._name, ATTR_ENTITY_ID: self.entity_id, } if "trigger" in variables and "description" in variables["trigger"]: event_data[ATTR_SOURCE] = variables["trigger"]["description"] - self.hass.bus.async_fire( - EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context - ) - self._logger.info("Executing %s", self._name) + @callback + def started_action(): + self.hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context + ) try: - await self.action_script.async_run(variables, trigger_context) + await self.action_script.async_run( + variables, trigger_context, started_action + ) except Exception: # pylint: disable=broad-except self._logger.exception("While executing automation %s", self.entity_id) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index ad0536ff3f9..adbacda6742 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -450,9 +450,7 @@ class _ScriptRun: ) except exceptions.TemplateError as ex: self._log( - "Error rendering event data template: %s", - ex, - level=logging.ERROR, + "Error rendering event data template: %s", ex, level=logging.ERROR ) self._hass.bus.async_fire( @@ -859,7 +857,10 @@ class Script: ).result() async def async_run( - self, variables: Optional[_VarsType] = None, context: Optional[Context] = None + self, + variables: Optional[_VarsType] = None, + context: Optional[Context] = None, + started_action: Optional[Callable[..., Any]] = None, ) -> None: """Run script.""" if context is None: @@ -894,6 +895,8 @@ class Script: self._hass, self, cast(dict, variables), context, self._log_exceptions ) self._runs.append(run) + if started_action: + self._hass.async_run_job(started_action) self.last_triggered = utcnow() self._changed() diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index b8f25dc9c46..8bbe28d3003 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -73,7 +73,7 @@ async def test_service_specify_data(hass, calls): time = dt_util.utcnow() - with patch("homeassistant.components.automation.utcnow", return_value=time): + with patch("homeassistant.helpers.script.utcnow", return_value=time): hass.bus.async_fire("test_event") await hass.async_block_till_done() @@ -587,11 +587,7 @@ async def test_automation_stops(hass, calls, service): ], } } - assert await async_setup_component( - hass, - automation.DOMAIN, - config, - ) + assert await async_setup_component(hass, automation.DOMAIN, config) running = asyncio.Event() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index ffbb8bb1cd7..364d635f506 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1706,3 +1706,22 @@ async def test_update_logger(hass, caplog): await hass.async_block_till_done() assert log_name in caplog.text + + +async def test_started_action(hass, caplog): + """Test the callback of started_action.""" + event = "test_event" + log_message = "The script started!" + logger = logging.getLogger("TEST") + + sequence = cv.SCRIPT_SCHEMA({"event": event}) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + @callback + def started_action(): + logger.info(log_message) + + await script_obj.async_run(context=Context(), started_action=started_action) + await hass.async_block_till_done() + + assert log_message in caplog.text From 5658a1efecf9bb0d3abfc719e85ee5e0123b880c Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Fri, 28 Aug 2020 22:05:11 +0200 Subject: [PATCH 416/862] Increase test coverage for rfxtrx integration (#39340) * Increase switch coverage * Increase binary sensor coverage * Final improvements * Remove debug statement * Adjust test duplicate cover * Remove None return test * Assert on length of conf_entries Co-authored-by: Chris Talkington Co-authored-by: Chris Talkington --- homeassistant/components/rfxtrx/__init__.py | 4 ++ tests/components/rfxtrx/test_binary_sensor.py | 61 +++++++++++++++++++ tests/components/rfxtrx/test_cover.py | 23 +++++++ tests/components/rfxtrx/test_switch.py | 22 +++++++ 4 files changed, 110 insertions(+) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 947a945eee7..a7f7425fb81 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -156,6 +156,8 @@ async def async_setup(hass, config): # Read device_id from the event code add to the data that will end up in the ConfigEntry for event_code, event_config in data[CONF_DEVICES].items(): event = get_rfx_object(event_code) + if event is None: + continue device_id = get_device_id( event.device, data_bits=event_config.get(CONF_DATA_BITS) ) @@ -229,6 +231,8 @@ def _get_device_lookup(devices): lookup = dict() for event_code, event_config in devices.items(): event = get_rfx_object(event_code) + if event is None: + continue device_id = get_device_id( event.device, data_bits=event_config.get(CONF_DATA_BITS) ) diff --git a/tests/components/rfxtrx/test_binary_sensor.py b/tests/components/rfxtrx/test_binary_sensor.py index 11efcfb6510..ee757192aaf 100644 --- a/tests/components/rfxtrx/test_binary_sensor.py +++ b/tests/components/rfxtrx/test_binary_sensor.py @@ -69,6 +69,35 @@ async def test_one_pt2262(hass, rfxtrx): assert state.state == "off" +async def test_pt2262_unconfigured(hass, rfxtrx): + """Test with discovery for PT2262.""" + assert await async_setup_component( + hass, + "rfxtrx", + { + "rfxtrx": { + "device": "abcd", + "devices": { + "0913000022670e013970": {}, + "09130000226707013970": {}, + }, + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + + state = hass.states.get("binary_sensor.pt2262_22670e") + assert state + assert state.state == "off" # probably aught to be unknown + assert state.attributes.get("friendly_name") == "PT2262 22670e" + + state = hass.states.get("binary_sensor.pt2262_226707") + assert state + assert state.state == "off" # probably aught to be unknown + assert state.attributes.get("friendly_name") == "PT2262 226707" + + @pytest.mark.parametrize( "state,event", [["on", "0b1100cd0213c7f230010f71"], ["off", "0b1100cd0213c7f230000f71"]], @@ -262,3 +291,35 @@ async def test_light(hass, rfxtrx_automatic): await rfxtrx.signal(EVENT_LIGHT_DETECTOR_DARK) assert hass.states.get(entity_id).state == "off" + + +async def test_pt2262_duplicate_id(hass, rfxtrx): + """Test with 1 sensor.""" + assert await async_setup_component( + hass, + "rfxtrx", + { + "rfxtrx": { + "device": "abcd", + "devices": { + "0913000022670e013970": { + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + }, + "09130000226707013970": { + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + }, + }, + } + }, + ) + await hass.async_block_till_done() + await hass.async_start() + + state = hass.states.get("binary_sensor.pt2262_22670e") + assert state + assert state.state == "off" # probably aught to be unknown + assert state.attributes.get("friendly_name") == "PT2262 22670e" diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index ce4838cebf1..05ce26ebc10 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -114,3 +114,26 @@ async def test_discover_covers(hass, rfxtrx_automatic): state = hass.states.get("cover.lightwaverf_siemens_f394ab_2") assert state assert state.state == "open" + + +async def test_duplicate_cover(hass, rfxtrx): + """Test with 2 duplicate covers.""" + assert await async_setup_component( + hass, + "rfxtrx", + { + "rfxtrx": { + "device": "abcd", + "devices": { + "0b1400cd0213c7f20d010f51": {}, + "0b1400cd0213c7f20d010f50": {}, + }, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.lightwaverf_siemens_0213c7_242") + assert state + assert state.state == "closed" + assert state.attributes.get("friendly_name") == "LightwaveRF, Siemens 0213c7:242" diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 3256f303708..1fed6a65562 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -3,6 +3,7 @@ from unittest.mock import call import pytest +from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import State from homeassistant.setup import async_setup_component @@ -151,3 +152,24 @@ async def test_discover_rfy_sun_switch(hass, rfxtrx_automatic): state = hass.states.get("switch.rfy_030101_1") assert state assert state.state == "on" + + +async def test_unknown_event_code(hass, rfxtrx): + """Test with 3 switches.""" + assert await async_setup_component( + hass, + "rfxtrx", + { + "rfxtrx": { + "device": "abcd", + "devices": {"1234567890": {}}, + } + }, + ) + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + assert len(conf_entries) == 1 + + entry = conf_entries[0] + assert entry.state == "loaded" From 94c474eab207c629bff98958c8945fc077985187 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 28 Aug 2020 22:09:46 +0200 Subject: [PATCH 417/862] Add missing status mappings for xiaomi_miio (#39357) --- .../components/xiaomi_miio/vacuum.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 106e8f9dfc4..3e414016fc5 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -91,20 +91,26 @@ SUPPORT_XIAOMI = ( STATE_CODE_TO_STATE = { - 2: STATE_IDLE, - 3: STATE_IDLE, - 5: STATE_CLEANING, - 6: STATE_RETURNING, - 7: STATE_CLEANING, - 8: STATE_DOCKED, - 9: STATE_ERROR, - 10: STATE_PAUSED, - 11: STATE_CLEANING, - 12: STATE_ERROR, - 15: STATE_RETURNING, - 16: STATE_CLEANING, - 17: STATE_CLEANING, - 18: STATE_CLEANING, + 1: STATE_IDLE, # "Starting" + 2: STATE_IDLE, # "Charger disconnected" + 3: STATE_IDLE, # "Idle" + 4: STATE_CLEANING, # "Remote control active" + 5: STATE_CLEANING, # "Cleaning" + 6: STATE_RETURNING, # "Returning home" + 7: STATE_CLEANING, # "Manual mode" + 8: STATE_DOCKED, # "Charging" + 9: STATE_ERROR, # "Charging problem" + 10: STATE_PAUSED, # "Paused" + 11: STATE_CLEANING, # "Spot cleaning" + 12: STATE_ERROR, # "Error" + 13: STATE_IDLE, # "Shutting down" + 14: STATE_DOCKED, # "Updating" + 15: STATE_RETURNING, # "Docking" + 16: STATE_CLEANING, # "Going to target" + 17: STATE_CLEANING, # "Zoned cleaning" + 18: STATE_CLEANING, # "Segment cleaning" + 100: STATE_DOCKED, # "Charging complete" + 101: STATE_ERROR, # "Device offline" } From c163d4a4b53399434f9adc3b2777798b13f103a5 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 28 Aug 2020 21:12:42 +0100 Subject: [PATCH 418/862] bump pymediaroom (#39360) --- homeassistant/components/mediaroom/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index 218715f81bf..c3a59e3404f 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -2,6 +2,6 @@ "domain": "mediaroom", "name": "Mediaroom", "documentation": "https://www.home-assistant.io/integrations/mediaroom", - "requirements": ["pymediaroom==0.6.4"], + "requirements": ["pymediaroom==0.6.4.1"], "codeowners": ["@dgomes"] } diff --git a/requirements_all.txt b/requirements_all.txt index 65d2e4cc161..8a3221cd355 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1460,7 +1460,7 @@ pymailgunner==1.4 pymata-express==1.13 # homeassistant.components.mediaroom -pymediaroom==0.6.4 +pymediaroom==0.6.4.1 # homeassistant.components.melcloud pymelcloud==2.5.2 From d587f134ca7d57bf2948c711fc9bd330aa48aa0c Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 28 Aug 2020 13:13:43 -0700 Subject: [PATCH 419/862] Reload mobile app notify service upon device name change, add device name to all webhook logs (#39364) * Add device name to all webhook logs to help with multiple devices * Reload notifications when we update the registration, update from rebase * Make hassfest happy * Adjust caplog test to accomodate log message change Co-authored-by: J. Nick Koston --- .../components/mobile_app/manifest.json | 2 +- .../components/mobile_app/webhook.py | 35 +++++++++++++------ tests/components/mobile_app/test_entity.py | 4 +-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 732a2495311..758df70c3d0 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": ["PyNaCl==1.3.0", "emoji==0.5.4"], "dependencies": ["http", "webhook", "person", "tag"], - "after_dependencies": ["cloud", "camera"], + "after_dependencies": ["cloud", "camera", "notify"], "codeowners": ["@robbiet480"], "quality_scale": "internal" } diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 9d614664a62..2f5e69fd02b 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -8,7 +8,7 @@ from aiohttp.web import HTTPBadRequest, Request, Response, json_response from nacl.secret import SecretBox import voluptuous as vol -from homeassistant.components import tag +from homeassistant.components import notify as hass_notify, tag from homeassistant.components.binary_sensor import ( DEVICE_CLASSES as BINARY_SENSOR_CLASSES, ) @@ -149,10 +149,12 @@ async def handle_webhook( config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] + device_name = config_entry.data[ATTR_DEVICE_NAME] + try: req_data = await request.json() except ValueError: - _LOGGER.warning("Received invalid JSON from mobile_app") + _LOGGER.warning("Received invalid JSON from mobile_app device: %s", device_name) return empty_okay_response(status=HTTP_BAD_REQUEST) if ( @@ -161,7 +163,7 @@ async def handle_webhook( ): _LOGGER.warning( "Refusing to accept unencrypted webhook from %s", - config_entry.data[ATTR_DEVICE_NAME], + device_name, ) return error_response(ERR_ENCRYPTION_REQUIRED, "Encryption required") @@ -169,7 +171,9 @@ async def handle_webhook( req_data = WEBHOOK_PAYLOAD_SCHEMA(req_data) except vol.Invalid as ex: err = vol.humanize.humanize_error(req_data, ex) - _LOGGER.error("Received invalid webhook payload: %s", err) + _LOGGER.error( + "Received invalid webhook from %s with payload: %s", device_name, err + ) return empty_okay_response() webhook_type = req_data[ATTR_WEBHOOK_TYPE] @@ -181,11 +185,11 @@ async def handle_webhook( webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) if webhook_type not in WEBHOOK_COMMANDS: - _LOGGER.error("Received invalid webhook type: %s", webhook_type) + _LOGGER.error( + "Received invalid webhook from %s of type: %s", device_name, webhook_type + ) return empty_okay_response() - device_name = config_entry.data[ATTR_DEVICE_NAME] - _LOGGER.debug( "Received webhook payload from %s for type %s: %s", device_name, @@ -348,6 +352,8 @@ async def webhook_update_registration(hass, config_entry, data): hass.config_entries.async_update_entry(config_entry, data=new_registration) + await hass_notify.async_reload(hass, DOMAIN) + return webhook_response( safe_registration(new_registration), registration=new_registration, @@ -403,6 +409,7 @@ async def webhook_register_sensor(hass, config_entry, data): """Handle a register sensor webhook.""" entity_type = data[ATTR_SENSOR_TYPE] unique_id = data[ATTR_SENSOR_UNIQUE_ID] + device_name = config_entry.data[ATTR_DEVICE_NAME] unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" existing_sensor = unique_store_key in hass.data[DOMAIN][entity_type] @@ -411,7 +418,9 @@ async def webhook_register_sensor(hass, config_entry, data): # If sensor already is registered, update current state instead if existing_sensor: - _LOGGER.debug("Re-register existing sensor %s", unique_id) + _LOGGER.debug( + "Re-register for %s of existing sensor %s", device_name, unique_id + ) entry = hass.data[DOMAIN][entity_type][unique_store_key] data = {**entry, **data} @@ -464,6 +473,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): } ) + device_name = config_entry.data[ATTR_DEVICE_NAME] resp = {} for sensor in data: entity_type = sensor[ATTR_SENSOR_TYPE] @@ -474,7 +484,9 @@ async def webhook_update_sensor_states(hass, config_entry, data): if unique_store_key not in hass.data[DOMAIN][entity_type]: _LOGGER.error( - "Refusing to update non-registered sensor: %s", unique_store_key + "Refusing to update %s non-registered sensor: %s", + device_name, + unique_store_key, ) err_msg = f"{entity_type} {unique_id} is not registered" resp[unique_id] = { @@ -490,7 +502,10 @@ async def webhook_update_sensor_states(hass, config_entry, data): except vol.Invalid as err: err_msg = vol.humanize.humanize_error(sensor, err) _LOGGER.error( - "Received invalid sensor payload for %s: %s", unique_id, err_msg + "Received invalid sensor payload from %s for %s: %s", + device_name, + unique_id, + err_msg, ) resp[unique_id] = { "success": False, diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index fd5baf50beb..df1493c084e 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -122,7 +122,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, ca assert reg_json == {"success": True} await hass.async_block_till_done() - assert "Re-register existing sensor" not in caplog.text + assert "Re-register" not in caplog.text entity = hass.states.get("sensor.test_1_battery_state") assert entity is not None @@ -143,7 +143,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, ca assert dupe_reg_json == {"success": True} await hass.async_block_till_done() - assert "Re-register existing sensor" in caplog.text + assert "Re-register" in caplog.text entity = hass.states.get("sensor.test_1_battery_state") assert entity is not None From 755ddf1a94ef7bbb3e16b621454784fce17e1792 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 28 Aug 2020 23:09:07 +0200 Subject: [PATCH 420/862] Add Netatmo camera light service (#39354) * Add camera light service * Move service to camera * Review --- homeassistant/components/netatmo/camera.py | 18 ++++++++++++++++++ homeassistant/components/netatmo/const.py | 7 +++++++ homeassistant/components/netatmo/light.py | 2 +- homeassistant/components/netatmo/services.yaml | 10 ++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index bb46e25af3f..210c9d92e3c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -11,9 +11,11 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( + ATTR_CAMERA_LIGHT_MODE, ATTR_PERSON, ATTR_PERSONS, ATTR_PSEUDO, + CAMERA_LIGHT_MODES, DATA_HANDLER, DATA_PERSONS, DOMAIN, @@ -21,6 +23,7 @@ from .const import ( EVENT_TYPE_ON, MANUFACTURER, MODELS, + SERVICE_SET_CAMERA_LIGHT, SERVICE_SET_PERSON_AWAY, SERVICE_SET_PERSONS_HOME, SIGNAL_NAME, @@ -101,6 +104,11 @@ async def async_setup_entry(hass, entry, async_add_entities): {vol.Optional(ATTR_PERSON): cv.string}, "_service_set_person_away", ) + platform.async_register_entity_service( + SERVICE_SET_CAMERA_LIGHT, + {vol.Required(ATTR_CAMERA_LIGHT_MODE): vol.In(CAMERA_LIGHT_MODES)}, + "_service_set_camera_light", + ) class NetatmoCamera(NetatmoBase, Camera): @@ -301,3 +309,13 @@ class NetatmoCamera(NetatmoBase, Camera): home_id=self._home_id, ) _LOGGER.debug("Set home as empty") + + def _service_set_camera_light(self, **kwargs): + """Service to set light mode.""" + mode = kwargs.get(ATTR_CAMERA_LIGHT_MODE) + _LOGGER.debug("Turn camera '%s' %s", self._name, mode) + self._data.set_state( + home_id=self._home_id, + camera_id=self._id, + floodlight=mode, + ) diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index f45fb09f94f..01351723716 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -67,7 +67,9 @@ ATTR_IS_KNOWN = "is_known" ATTR_FACE_URL = "face_url" ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_NAME = "schedule_name" +ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" +SERVICE_SET_CAMERA_LIGHT = "set_camera_light" SERVICE_SET_SCHEDULE = "set_schedule" SERVICE_SET_PERSONS_HOME = "set_persons_home" SERVICE_SET_PERSON_AWAY = "set_person_away" @@ -78,3 +80,8 @@ EVENT_TYPE_OFF = "off" EVENT_TYPE_ON = "on" EVENT_TYPE_SET_POINT = "set_point" EVENT_TYPE_THERM_MODE = "therm_mode" + +MODE_LIGHT_ON = "on" +MODE_LIGHT_OFF = "off" +MODE_LIGHT_AUTO = "auto" +CAMERA_LIGHT_MODES = [MODE_LIGHT_ON, MODE_LIGHT_OFF, MODE_LIGHT_AUTO] diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index a41dae33641..0c9e0c53176 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -142,7 +142,7 @@ class NetatmoLight(NetatmoBase, LightEntity): def turn_off(self, **kwargs): """Turn camera floodlight into auto mode.""" - _LOGGER.debug("Turn camera '%s' off", self._name) + _LOGGER.debug("Turn camera '%s' to auto mode", self._name) self._data.set_state( home_id=self._home_id, camera_id=self._id, diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index bd8a0cc8f20..459ef23b0e0 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -1,4 +1,14 @@ # Describes the format for available Netatmo services +set_camera_light: + description: Set the camera light mode. + fields: + camera_light_mode: + description: Outdoor camera light mode (on/off/auto) + example: auto + entity_id: + description: Entity id of the camera. + example: camera.netatmo_entrance + set_schedule: description: Set the heating schedule. fields: From 16ad8cf720ee8b115d06610f0d62a0f38f622307 Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Fri, 28 Aug 2020 14:43:40 -0700 Subject: [PATCH 421/862] Fix todoist calendar events (#39197) Updated the calendar event dict to contain a `summary` key so that the title will display on the calendar panel. Also update the start/end date to not include time information if the event is all day so that it renders as an all day event on the calendar panel. --- homeassistant/components/todoist/calendar.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 014dbd37a4e..548cac6da92 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -503,12 +503,19 @@ class TodoistProjectData: continue due_date = _parse_due_date(task["due"]) if start_date < due_date < end_date: + if due_date.hour == 0 and due_date.minute == 0: + # If the due date has no time data, return just the date so that it + # will render correctly as an all day event on a calendar. + due_date_value = due_date.strftime("%Y-%m-%d") + else: + due_date_value = due_date.isoformat() event = { "uid": task["id"], "title": task["content"], - "start": due_date.isoformat(), - "end": due_date.isoformat(), + "start": due_date_value, + "end": due_date_value, "allDay": True, + "summary": task["content"], } events.append(event) return events From 989a040b67cc24616cca3f9069142fcc21beb2b7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 29 Aug 2020 00:03:59 +0000 Subject: [PATCH 422/862] [ci skip] Translation update --- .../components/airvisual/translations/fr.json | 4 +-- .../components/atag/translations/fr.json | 2 +- .../components/blink/translations/fr.json | 2 +- .../components/broadlink/translations/fr.json | 28 +++++++++++++-- .../components/bsblan/translations/fr.json | 2 +- .../components/daikin/translations/fr.json | 3 +- .../devolo_home_control/translations/fr.json | 3 ++ .../components/directv/translations/fr.json | 4 +-- .../components/doorbird/translations/fr.json | 2 +- .../components/elkm1/translations/fr.json | 4 +-- .../homekit_controller/translations/fr.json | 8 +++++ .../components/insteon/translations/fr.json | 2 ++ .../components/ipp/translations/fr.json | 6 ++-- .../components/kodi/translations/fr.json | 35 ++++++++++++++++++- .../components/konnected/translations/fr.json | 2 +- .../components/melcloud/translations/fr.json | 4 +-- .../meteo_france/translations/fr.json | 19 ++++++++++ .../components/monoprice/translations/fr.json | 2 +- .../components/mqtt/translations/fr.json | 14 +++++++- .../components/netatmo/translations/fr.json | 8 ++++- .../nightscout/translations/fr.json | 7 ++++ .../ovo_energy/translations/fr.json | 7 ++++ .../components/pi_hole/translations/fr.json | 2 +- .../components/plex/translations/fr.json | 2 ++ .../components/rachio/translations/fr.json | 2 +- .../components/risco/translations/ca.json | 22 ++++++++++++ .../components/risco/translations/en.json | 22 ++++++++++++ .../components/risco/translations/fr.json | 24 +++++++++++++ .../components/risco/translations/no.json | 22 ++++++++++++ .../components/risco/translations/ru.json | 22 ++++++++++++ .../risco/translations/zh-Hant.json | 22 ++++++++++++ .../components/roku/translations/fr.json | 4 +-- .../components/roon/translations/fr.json | 10 +++++- .../components/sensor/translations/fr.json | 12 +++++-- .../components/sentry/translations/fr.json | 6 ++-- .../components/shelly/translations/fr.json | 11 ++++++ .../smart_meter_texas/translations/fr.json | 12 +++++++ .../components/spotify/translations/fr.json | 7 +++- .../components/spotify/translations/no.json | 7 +++- .../components/spotify/translations/ru.json | 7 +++- .../spotify/translations/zh-Hant.json | 7 +++- .../components/tuya/translations/fr.json | 13 ++++++- .../components/vacuum/translations/fr.json | 4 +-- .../components/vizio/translations/fr.json | 12 ++++--- .../components/wilight/translations/fr.json | 2 ++ .../components/wled/translations/fr.json | 4 +++ .../xiaomi_aqara/translations/fr.json | 10 ++++-- .../xiaomi_miio/translations/fr.json | 3 +- .../components/zwave/translations/fr.json | 4 +-- 49 files changed, 394 insertions(+), 49 deletions(-) create mode 100644 homeassistant/components/nightscout/translations/fr.json create mode 100644 homeassistant/components/ovo_energy/translations/fr.json diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index 8d9eef019e0..7bd42083e6d 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -21,7 +21,7 @@ "node_pro": { "data": { "ip_address": "Adresse IP / nom d'h\u00f4te de l'unit\u00e9", - "password": "Mot de passe de l'unit\u00e9" + "password": "Mot de passe" }, "description": "Surveillez une unit\u00e9 AirVisual personnelle. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.", "title": "Configurer un AirVisual Node/Pro" @@ -32,7 +32,7 @@ "node_pro": "AirVisual Node Pro", "type": "Type d'int\u00e9gration" }, - "description": "Surveiller la qualit\u00e9 de l\u2019air dans un emplacement g\u00e9ographique.", + "description": "Choisissez le type de donn\u00e9es AirVisual que vous souhaitez surveiller.", "title": "Configurer AirVisual" } } diff --git a/homeassistant/components/atag/translations/fr.json b/homeassistant/components/atag/translations/fr.json index 32a752402f3..6652d1508a7 100644 --- a/homeassistant/components/atag/translations/fr.json +++ b/homeassistant/components/atag/translations/fr.json @@ -12,7 +12,7 @@ "data": { "email": "Courriel (facultatif)", "host": "Nom d'h\u00f4te ou adresse IP", - "port": "Port (10000)" + "port": "Port" }, "title": "Se connecter \u00e0 l'appareil" } diff --git a/homeassistant/components/blink/translations/fr.json b/homeassistant/components/blink/translations/fr.json index 68ba9285fe4..80468b36409 100644 --- a/homeassistant/components/blink/translations/fr.json +++ b/homeassistant/components/blink/translations/fr.json @@ -12,7 +12,7 @@ "data": { "2fa": "Code \u00e0 deux facteurs" }, - "description": "Saisissez le code envoy\u00e9 \u00e0 votre adresse \u00e9lectronique. Si l'e-mail ne contient pas de code PIN, laissez vide", + "description": "Entrez le code PIN envoy\u00e9 \u00e0 votre e-mail", "title": "Authentification \u00e0 deux facteurs" }, "user": { diff --git a/homeassistant/components/broadlink/translations/fr.json b/homeassistant/components/broadlink/translations/fr.json index c3fd625352b..2bf2477f615 100644 --- a/homeassistant/components/broadlink/translations/fr.json +++ b/homeassistant/components/broadlink/translations/fr.json @@ -1,12 +1,18 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Il y a d\u00e9j\u00e0 un processus de configuration en cours pour cet appareil", - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "unknown": "Erreur inattendue" }, "error": { - "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide" + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "unknown": "Erreur inattendue" }, + "flow_title": "{name} ( {model} \u00e0 {host} )", "step": { "auth": { "title": "S'authentifier sur l'appareil" @@ -16,6 +22,24 @@ "name": "Nom" }, "title": "Choisissez un nom pour l'appareil" + }, + "reset": { + "description": "Votre appareil est verrouill\u00e9 pour l'authentification. Suivez les instructions pour le d\u00e9verrouiller: \n 1. R\u00e9initialisez l'appareil aux param\u00e8tres d'usine. \n 2. Utilisez l'application officielle pour ajouter l'appareil \u00e0 votre r\u00e9seau local. \n 3. Arr\u00eatez. Ne terminez pas la configuration. Fermez l'appli. \n 4. Cliquez sur Soumettre.", + "title": "D\u00e9verrouiller l'appareil" + }, + "unlock": { + "data": { + "unlock": "Oui, le faire." + }, + "description": "Votre appareil est verrouill\u00e9. Cela peut entra\u00eener des probl\u00e8mes d'authentification dans Home Assistant. Souhaitez-vous le d\u00e9verrouiller ?", + "title": "D\u00e9verrouiller l'appareil (facultatif)" + }, + "user": { + "data": { + "host": "H\u00f4te", + "timeout": "D\u00e9lai expir\u00e9" + }, + "title": "Se connecter \u00e0 l'appareil" } } } diff --git a/homeassistant/components/bsblan/translations/fr.json b/homeassistant/components/bsblan/translations/fr.json index 25516d41645..48b235aa177 100644 --- a/homeassistant/components/bsblan/translations/fr.json +++ b/homeassistant/components/bsblan/translations/fr.json @@ -12,7 +12,7 @@ "data": { "host": "Nom d'h\u00f4te ou adresse IP", "passkey": "Cha\u00eene de cl\u00e9 d'acc\u00e8s", - "port": "Num\u00e9ro de port" + "port": "Port" }, "description": "Configurez votre appareil BSB-Lan pour l'int\u00e9grer \u00e0 HomeAssistant.", "title": "Connectez-vous \u00e0 l'appareil BSB-Lan" diff --git a/homeassistant/components/daikin/translations/fr.json b/homeassistant/components/daikin/translations/fr.json index 49f05b33679..7711b8bc045 100644 --- a/homeassistant/components/daikin/translations/fr.json +++ b/homeassistant/components/daikin/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion" }, "error": { "device_fail": "Erreur inattendue", diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index 0ef34dc5bd4..1f9b6f4e28f 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Cette unit\u00e9 centrale Home Control est d\u00e9j\u00e0 utilis\u00e9e." + }, "error": { "invalid_credentials": "Nom d''utilisateur et/ou mot de passe incorrect." }, diff --git a/homeassistant/components/directv/translations/fr.json b/homeassistant/components/directv/translations/fr.json index 5516d1c2c60..4876c455ff0 100644 --- a/homeassistant/components/directv/translations/fr.json +++ b/homeassistant/components/directv/translations/fr.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Le r\u00e9cepteur DirecTV est d\u00e9j\u00e0 configur\u00e9", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "unknown": "Erreur inattendue" }, "error": { - "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" + "cannot_connect": "\u00c9chec de connexion" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json index b211d8f85d8..304760dbf58 100644 --- a/homeassistant/components/doorbird/translations/fr.json +++ b/homeassistant/components/doorbird/translations/fr.json @@ -6,7 +6,7 @@ "not_doorbird_device": "Cet appareil n'est pas un DoorBird" }, "error": { - "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/elkm1/translations/fr.json b/homeassistant/components/elkm1/translations/fr.json index 2dfa3ba8347..81265e587b2 100644 --- a/homeassistant/components/elkm1/translations/fr.json +++ b/homeassistant/components/elkm1/translations/fr.json @@ -13,10 +13,10 @@ "user": { "data": { "address": "L'adresse IP ou le domaine ou le port s\u00e9rie si vous vous connectez via s\u00e9rie.", - "password": "Mot de passe (s\u00e9curis\u00e9 uniquement).", + "password": "Mot de passe", "prefix": "Un pr\u00e9fixe unique (laissez vide si vous n'avez qu'un seul ElkM1).", "protocol": "Protocole", - "username": "Nom d'utilisateur (s\u00e9curis\u00e9 uniquement)." + "username": "Nom d'utilisateur" }, "title": "Se connecter a Elk-M1 Control" } diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 6a0385737ce..1e5671a67af 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -20,6 +20,14 @@ }, "flow_title": "Accessoire HomeKit: {name}", "step": { + "busy_error": { + "description": "Annulez l'association sur tous les contr\u00f4leurs ou essayez de red\u00e9marrer l'appareil, puis continuez \u00e0 reprendre l'association.", + "title": "L'appareil est d\u00e9j\u00e0 coupl\u00e9 avec un autre contr\u00f4leur" + }, + "max_tries_error": { + "description": "L'appareil a re\u00e7u plus de 100 tentatives d'authentification infructueuses. Essayez de red\u00e9marrer l'appareil, puis continuez pour reprendre l'association.", + "title": "Nombre maximal de tentatives d'authentification d\u00e9pass\u00e9" + }, "pair": { "data": { "pairing_code": "Code d\u2019appairage" diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index 9922cedb0a3..4962f03e621 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -31,6 +31,8 @@ }, "init": { "data": { + "add_x10": "Ajouter un appareil X10.", + "change_hub_config": "Modifier la configuration du Hub.", "remove_x10": "Retirez un p\u00e9riph\u00e9rique X10." }, "description": "S\u00e9lectionnez une option \u00e0 configurer.", diff --git a/homeassistant/components/ipp/translations/fr.json b/homeassistant/components/ipp/translations/fr.json index 789f5d56b13..be2166ad433 100644 --- a/homeassistant/components/ipp/translations/fr.json +++ b/homeassistant/components/ipp/translations/fr.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Cette imprimante est d\u00e9j\u00e0 configur\u00e9e.", - "connection_error": "Impossible de se connecter \u00e0 l'imprimante.", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "connection_error": "\u00c9chec de connexion", "connection_upgrade": "Impossible de se connecter \u00e0 l'imprimante parce qu'une mise \u00e0 niveau de la connexion est n\u00e9cessaire.", "ipp_error": "Erreur IPP rencontr\u00e9e.", "ipp_version_error": "Version d'IPP non prise en charge par l'imprimante.", @@ -10,7 +10,7 @@ "unique_id_required": "Dispositif ne portant pas l'identification unique requise pour la d\u00e9couverte." }, "error": { - "connection_error": "Impossible de se connecter \u00e0 l'imprimante.", + "connection_error": "\u00c9chec de connexion", "connection_upgrade": "Impossible de se connecter \u00e0 l'imprimante. Veuillez r\u00e9essayer avec l'option SSL / TLS coch\u00e9e." }, "flow_title": "Imprimante: {name}", diff --git a/homeassistant/components/kodi/translations/fr.json b/homeassistant/components/kodi/translations/fr.json index 5b3c0e15021..9c64ce2efdd 100644 --- a/homeassistant/components/kodi/translations/fr.json +++ b/homeassistant/components/kodi/translations/fr.json @@ -1,17 +1,50 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification erron\u00e9e", "unknown": "Erreur inattendue" }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, "flow_title": "Kodi: {name}", "step": { + "credentials": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Veuillez entrer votre nom d'utilisateur et votre mot de passe Kodi. Ceux-ci peuvent \u00eatre trouv\u00e9s dans Syst\u00e8me / Param\u00e8tres / R\u00e9seau / Services." + }, + "discovery_confirm": { + "description": "Voulez-vous ajouter Kodi (` {name} `) \u00e0 Home Assistant ?", + "title": "Kodi d\u00e9couvert" + }, + "host": { + "data": { + "host": "H\u00f4te", + "port": "Port", + "ssl": "Connexion via SSL" + }, + "description": "Informations de connexion Kodi. Veuillez vous assurer d'activer \"Autoriser le contr\u00f4le de Kodi via HTTP\" dans Syst\u00e8me / Param\u00e8tres / R\u00e9seau / Services." + }, "user": { "data": { "host": "H\u00f4te", "port": "Port", "ssl": "Connexion via SSL" - } + }, + "description": "Informations de connexion Kodi. Veuillez vous assurer d'activer \"Autoriser le contr\u00f4le de Kodi via HTTP\" dans Syst\u00e8me / Param\u00e8tres / R\u00e9seau / Services." + }, + "ws_port": { + "data": { + "ws_port": "Port" + }, + "description": "Le port WebSocket (parfois appel\u00e9 port TCP dans Kodi). Pour vous connecter via WebSocket, vous devez activer \"Autoriser les programmes ... \u00e0 contr\u00f4ler Kodi\" dans Syst\u00e8me / Param\u00e8tres / R\u00e9seau / Services. Si WebSocket n'est pas activ\u00e9, supprimez le port et laissez vide." } } }, diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index d4fcfdf6500..be48b5e15c2 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -21,7 +21,7 @@ "user": { "data": { "host": "Adresse IP", - "port": "Port de l'appareil Konnected" + "port": "Port" }, "description": "Veuillez saisir les informations de l\u2019h\u00f4te de votre panneau Konnected." } diff --git a/homeassistant/components/melcloud/translations/fr.json b/homeassistant/components/melcloud/translations/fr.json index a567058297e..1ee577e0bfa 100644 --- a/homeassistant/components/melcloud/translations/fr.json +++ b/homeassistant/components/melcloud/translations/fr.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "password": "Mot de passe MELCloud.", - "username": "E-mail utilis\u00e9e pour vous connecter \u00e0 MELCloud." + "password": "Mot de passe", + "username": "Email" }, "description": "Se connecter en utilisant votre MELCloud compte.", "title": "Se connecter \u00e0 MELCloud" diff --git a/homeassistant/components/meteo_france/translations/fr.json b/homeassistant/components/meteo_france/translations/fr.json index 9ad5f298fba..8e321092d63 100644 --- a/homeassistant/components/meteo_france/translations/fr.json +++ b/homeassistant/components/meteo_france/translations/fr.json @@ -4,7 +4,17 @@ "already_configured": "Ville d\u00e9j\u00e0 configur\u00e9e", "unknown": "Erreur inconnue: veuillez r\u00e9essayer plus tard" }, + "error": { + "empty": "Aucun r\u00e9sultat dans la recherche par ville: veuillez v\u00e9rifier le champ de la ville" + }, "step": { + "cities": { + "data": { + "city": "Ville" + }, + "description": "Choisissez votre ville dans la liste", + "title": "M\u00e9t\u00e9o-France" + }, "user": { "data": { "city": "Ville" @@ -13,5 +23,14 @@ "title": "M\u00e9t\u00e9o-France" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Mode de pr\u00e9vision" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/fr.json b/homeassistant/components/monoprice/translations/fr.json index 63b3adc4a2a..9a0ffa2354d 100644 --- a/homeassistant/components/monoprice/translations/fr.json +++ b/homeassistant/components/monoprice/translations/fr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "port": "Port s\u00e9rie", + "port": "Port", "source_1": "Nom de la source #1", "source_2": "Nom de la source #2", "source_3": "Nom de la source #3", diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index 72c5bbae643..574db2d2faf 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -50,6 +50,8 @@ }, "options": { "error": { + "bad_birth": "Topic de naissance invalide", + "bad_will": "Topic de testament invalide", "cannot_connect": "Impossible de se connecter au broker." }, "step": { @@ -64,7 +66,17 @@ }, "options": { "data": { - "discovery": "Activer la d\u00e9couverte" + "birth_enable": "Activer le message de naissance", + "birth_payload": "Contenu du message de naissance", + "birth_qos": "QoS du message de naissance", + "birth_retain": "Retenir le message de naissance", + "birth_topic": "Topic du message de naissance", + "discovery": "Activer la d\u00e9couverte", + "will_enable": "Activer le message de naissance", + "will_payload": "Contenu du message de testament", + "will_qos": "QoS du message de testament", + "will_retain": "Retenir le message de testament", + "will_topic": "Topic du message de testament" }, "description": "Veuillez s\u00e9lectionner les options MQTT." } diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index 7a269a23d70..abb1864512b 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "missing_configuration": "Ce composant n'est pas configur\u00e9. Veuillez suivre la documentation." + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "missing_configuration": "Ce composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { "default": "Authentification r\u00e9ussie avec Netatmo." @@ -17,6 +19,10 @@ "public_weather": { "data": { "area_name": "Nom de la zone", + "lat_ne": "Latitude nord-est", + "lat_sw": "Latitude sud-ouest", + "lon_ne": "Longitude nord-est", + "lon_sw": "Longitude sud-ouest", "mode": "Calcul", "show_on_map": "Montrer sur la carte" }, diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json new file mode 100644 index 00000000000..c4bc0d48b1a --- /dev/null +++ b/homeassistant/components/nightscout/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json new file mode 100644 index 00000000000..546b76a1802 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "connection_error": "\u00c9chec de connexion" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json index 2e068cf0036..0e56dfaa0d9 100644 --- a/homeassistant/components/pi_hole/translations/fr.json +++ b/homeassistant/components/pi_hole/translations/fr.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "Cl\u00e9 API (facultatif)", + "api_key": "Cl\u00e9 d'API", "host": "H\u00f4te", "name": "Nom", "port": "Port", diff --git a/homeassistant/components/plex/translations/fr.json b/homeassistant/components/plex/translations/fr.json index eea3f5f678a..685eab31bfd 100644 --- a/homeassistant/components/plex/translations/fr.json +++ b/homeassistant/components/plex/translations/fr.json @@ -9,6 +9,7 @@ }, "error": { "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", + "host_or_token": "Doit fournir au moins un h\u00f4te ou un jeton", "no_servers": "Aucun serveur li\u00e9 au compte", "not_found": "Serveur Plex introuvable", "ssl_error": "Probl\u00e8me de certificat SSL" @@ -33,6 +34,7 @@ "title": "S\u00e9lectionnez le serveur Plex" }, "user": { + "description": "Continuez sur [plex.tv] (https://plex.tv) pour lier un serveur Plex.", "title": "Plex Media Server" }, "user_advanced": { diff --git a/homeassistant/components/rachio/translations/fr.json b/homeassistant/components/rachio/translations/fr.json index 2278c7d3e1b..a52c0bd6d4a 100644 --- a/homeassistant/components/rachio/translations/fr.json +++ b/homeassistant/components/rachio/translations/fr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_key": "La cl\u00e9 API pour le compte Rachio." + "api_key": "Cl\u00e9 d'API" }, "description": "Vous aurez besoin de la cl\u00e9 API de https://app.rach.io/. S\u00e9lectionnez \"Param\u00e8tres du compte, puis cliquez sur \"GET API KEY \".", "title": "Connectez-vous \u00e0 votre appareil Rachio" diff --git a/homeassistant/components/risco/translations/ca.json b/homeassistant/components/risco/translations/ca.json index 693885d51cb..6bfce849330 100644 --- a/homeassistant/components/risco/translations/ca.json +++ b/homeassistant/components/risco/translations/ca.json @@ -20,6 +20,16 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "Activat, mode fora", + "armed_custom_bypass": "Activat, bypass personalitzat", + "armed_home": "Activat, mode a casa", + "armed_night": "Activat, mode nocturn" + }, + "description": "Selecciona quin estat establir a l'alarma Risco quan s'activi l'alarma de Home Assistant", + "title": "Converteix els estats de Risco en estats a Home Assistant" + }, "init": { "data": { "code_arm_required": "Demana codi PIN per activar", @@ -27,6 +37,18 @@ "scan_interval": "Freq\u00fc\u00e8ncia de sondeig a Risco (en segons)" }, "title": "Opcions de configuraci\u00f3" + }, + "risco_to_ha": { + "data": { + "A": "Grup A", + "B": "Grup B", + "C": "Grup C", + "D": "Grup D", + "arm": "Activat (mode fora)", + "partial_arm": "Activat parcial (estada)" + }, + "description": "Selecciona quin estat tindr\u00e0 l'alarma de Home Assistant per a cadascun dels estats que proporcioni Risco", + "title": "Converteix els estats de Risco en estats a Home Assistant" } } } diff --git a/homeassistant/components/risco/translations/en.json b/homeassistant/components/risco/translations/en.json index 82422dcbe68..37c2343fe75 100644 --- a/homeassistant/components/risco/translations/en.json +++ b/homeassistant/components/risco/translations/en.json @@ -20,6 +20,16 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "Armed Away", + "armed_custom_bypass": "Armed Custom Bypass", + "armed_home": "Armed Home", + "armed_night": "Armed Night" + }, + "description": "Select what state to set your Risco alarm to when arming the Home Assistant alarm", + "title": "Map Home Assistant states to Risco states" + }, "init": { "data": { "code_arm_required": "Require pin code to arm", @@ -27,6 +37,18 @@ "scan_interval": "How often to poll Risco (in seconds)" }, "title": "Configure options" + }, + "risco_to_ha": { + "data": { + "A": "Group A", + "B": "Group B", + "C": "Group C", + "D": "Group D", + "arm": "Armed (AWAY)", + "partial_arm": "Partially Armed (STAY)" + }, + "description": "Select what state your Home Assistant alarm will report for every state reported by Risco", + "title": "Map Risco states to Home Assistant states" } } } diff --git a/homeassistant/components/risco/translations/fr.json b/homeassistant/components/risco/translations/fr.json index 827893b2194..69224a3e8b1 100644 --- a/homeassistant/components/risco/translations/fr.json +++ b/homeassistant/components/risco/translations/fr.json @@ -20,11 +20,35 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "Arm\u00e9 absent", + "armed_custom_bypass": "Arm\u00e9 avec exception personnalis\u00e9e", + "armed_home": "Arm\u00e9 pr\u00e9sent", + "armed_night": "Arm\u00e9 pour la nuit" + }, + "description": "S\u00e9lectionnez l'\u00e9tat dans lequel r\u00e9gler votre alarme Risco lors de l'armement de l'alarme Home Assistant", + "title": "Associer les \u00e9tats de Home Assistant aux \u00e9tats de Risco" + }, "init": { "data": { + "code_arm_required": "Exiger un code PIN pour armer", + "code_disarm_required": "Exiger un code PIN pour d\u00e9sarmer", "scan_interval": "\u00c0 quelle fr\u00e9quence interroger Risco (en secondes)" }, "title": "Configurer les options" + }, + "risco_to_ha": { + "data": { + "A": "Groupe A", + "B": "Groupe B", + "C": "Groupe C", + "D": "Groupe D", + "arm": "Arm\u00e9 (Absent)", + "partial_arm": "Partiellement arm\u00e9 (PRESENT)" + }, + "description": "S\u00e9lectionnez l'\u00e9tat que votre alarme Home Assistant rapportera pour chaque \u00e9tat signal\u00e9 par Risco", + "title": "Associer les \u00e9tats de Risco aux \u00e9tats de Home Assistant" } } } diff --git a/homeassistant/components/risco/translations/no.json b/homeassistant/components/risco/translations/no.json index 5ab1d507b29..48f966bd37c 100644 --- a/homeassistant/components/risco/translations/no.json +++ b/homeassistant/components/risco/translations/no.json @@ -20,6 +20,16 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "Sikret Borte", + "armed_custom_bypass": "Sikret tilpasset bypass", + "armed_home": "Sikret Hjemme", + "armed_night": "Sikret Natt" + }, + "description": "Velg hvilken tilstand du vil stille Risco-alarmen til n\u00e5r du aktiverer Home Assistant-alarmen", + "title": "Kart Hjem Assistant oppgir til Risco stater" + }, "init": { "data": { "code_arm_required": "Krev PIN-kode for \u00e5 koble til", @@ -27,6 +37,18 @@ "scan_interval": "Hvor ofte skal man unders\u00f8ke Risco (i l\u00f8pet av sekunder)" }, "title": "Konfigurer alternativer" + }, + "risco_to_ha": { + "data": { + "A": "Gruppe A", + "B": "Gruppe B", + "C": "Gruppe C", + "D": "Gruppe D", + "arm": "Sikret (BORTE)", + "partial_arm": "Delvis Sikret (OPPHOLD)" + }, + "description": "Velg hvilken tilstand Home Assistant-alarmen skal rapportere for hver delstat som er rapportert av Risco", + "title": "Kart Risco oppgir til Home Assistant stater" } } } diff --git a/homeassistant/components/risco/translations/ru.json b/homeassistant/components/risco/translations/ru.json index b4f55347512..3fd1fd567f9 100644 --- a/homeassistant/components/risco/translations/ru.json +++ b/homeassistant/components/risco/translations/ru.json @@ -20,6 +20,16 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "\u041e\u0445\u0440\u0430\u043d\u0430 (\u043d\u0435 \u0434\u043e\u043c\u0430)", + "armed_custom_bypass": "\u041e\u0445\u0440\u0430\u043d\u0430 \u0441 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c\u0438", + "armed_home": "\u041e\u0445\u0440\u0430\u043d\u0430 (\u0434\u043e\u043c\u0430)", + "armed_night": "\u041e\u0445\u0440\u0430\u043d\u0430 (\u043d\u043e\u0447\u044c)" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Risco \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Home Assistant", + "title": "\u0421\u043e\u043f\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 Home Assistant \u0438 Risco" + }, "init": { "data": { "code_arm_required": "\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c PIN-\u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", @@ -27,6 +37,18 @@ "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "risco_to_ha": { + "data": { + "A": "\u0413\u0440\u0443\u043f\u043f\u0430 \u0410", + "B": "\u0413\u0440\u0443\u043f\u043f\u0430 B", + "C": "\u0413\u0440\u0443\u043f\u043f\u0430 C", + "D": "\u0413\u0440\u0443\u043f\u043f\u0430 D", + "arm": "\u041e\u0445\u0440\u0430\u043d\u0430 (AWAY)", + "partial_arm": "\u0427\u0430\u0441\u0442\u0438\u0447\u043d\u0430\u044f \u043e\u0445\u0440\u0430\u043d\u0430 (STAY)" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Home Assistant \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Risco", + "title": "\u0421\u043e\u043f\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 Home Assistant \u0438 Risco" } } } diff --git a/homeassistant/components/risco/translations/zh-Hant.json b/homeassistant/components/risco/translations/zh-Hant.json index 0a703baf68e..ae4c3f5675a 100644 --- a/homeassistant/components/risco/translations/zh-Hant.json +++ b/homeassistant/components/risco/translations/zh-Hant.json @@ -20,6 +20,16 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "\u96e2\u5bb6\u8b66\u6212", + "armed_custom_bypass": "\u8b66\u6212\u6a21\u5f0f\u72c0\u614b", + "armed_home": "\u5728\u5bb6\u8b66\u6212", + "armed_night": "\u591c\u9593\u8b66\u6212" + }, + "description": "\u9078\u64c7\u7531 Home Assistant \u56de\u5831\u72c0\u614b\u4e2d\uff0c\u900f\u904e Risco \u56de\u5831\u7684\u72c0\u614b\u8b66\u5831", + "title": "\u5c07 Home Assistant \u72c0\u614b\u5c0d\u61c9\u81f3 Risco" + }, "init": { "data": { "code_arm_required": "\u9700\u8981\u8f38\u5165 PIN \u4ee5\u8b66\u6212", @@ -27,6 +37,18 @@ "scan_interval": "\u66f4\u65b0 Risco \u983b\u7387\uff08\u79d2\uff09" }, "title": "\u8a2d\u5b9a\u9078\u9805" + }, + "risco_to_ha": { + "data": { + "A": "A \u7d44", + "B": "B \u7d44", + "C": "C \u7d44", + "D": "D \u7d44", + "arm": "\u8b66\u6212\uff08\u96e2\u5bb6\uff09", + "partial_arm": "\u90e8\u5206\u8b66\u6212\uff08\u66ab\u7559\uff09" + }, + "description": "\u9078\u64c7\u7531 Risco \u56de\u5831\u72c0\u614b\u4e2d\uff0c\u900f\u904e Home Assistant \u56de\u5831\u7684\u72c0\u614b\u8b66\u5831", + "title": "\u5c07 Risco \u72c0\u614b\u5c0d\u61c9\u81f3 Home Assistant" } } } diff --git a/homeassistant/components/roku/translations/fr.json b/homeassistant/components/roku/translations/fr.json index 7df15f13015..7aba1ef0489 100644 --- a/homeassistant/components/roku/translations/fr.json +++ b/homeassistant/components/roku/translations/fr.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Le p\u00e9riph\u00e9rique Roku est d\u00e9j\u00e0 configur\u00e9", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "unknown": "Erreur inattendue" }, "error": { - "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" + "cannot_connect": "\u00c9chec de connexion" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roon/translations/fr.json b/homeassistant/components/roon/translations/fr.json index 2db0855979e..c282a4cfdbf 100644 --- a/homeassistant/components/roon/translations/fr.json +++ b/homeassistant/components/roon/translations/fr.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, "error": { - "duplicate_entry": "Cet h\u00f4te a d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9." + "duplicate_entry": "Cet h\u00f4te a d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9.", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" }, "step": { "link": { @@ -9,6 +14,9 @@ "title": "Autoriser Home Assistant dans Roon" }, "user": { + "data": { + "host": "H\u00f4te" + }, "description": "Veuillez entrer votre nom d\u2019h\u00f4te ou votre adresse IP sur votre serveur Roon.", "title": "Configurer le serveur Roon" } diff --git a/homeassistant/components/sensor/translations/fr.json b/homeassistant/components/sensor/translations/fr.json index 8f7cbf0f8aa..4705d28b5c3 100644 --- a/homeassistant/components/sensor/translations/fr.json +++ b/homeassistant/components/sensor/translations/fr.json @@ -2,25 +2,33 @@ "device_automation": { "condition_type": { "is_battery_level": "Niveau de la batterie de {entity_name}", + "is_current": "Courant actuel pour {entity_name}", + "is_energy": "\u00c9nergie actuelle pour {entity_name}", "is_humidity": "Humidit\u00e9 de {entity_name}", "is_illuminance": "\u00c9clairement de {entity_name}", "is_power": "Puissance de {entity_name}", + "is_power_factor": "Facteur de puissance actuel pour {entity_name}", "is_pressure": "Pression de {entity_name}", "is_signal_strength": "Force du signal de {entity_name}", "is_temperature": "Temp\u00e9rature de {entity_name}", "is_timestamp": "Horodatage de {entity_name}", - "is_value": "La valeur actuelle de {entity_name}" + "is_value": "La valeur actuelle de {entity_name}", + "is_voltage": "Tension actuelle pour {entity_name}" }, "trigger_type": { "battery_level": "{entity_name} modification du niveau de batterie", + "current": "{entity_name} changement de courant", + "energy": "{entity_name} changement d'\u00e9nergie", "humidity": "{entity_name} modification de l'humidit\u00e9", "illuminance": "{entity_name} modification de l'\u00e9clairement", "power": "{entity_name} modification de la puissance", + "power_factor": "{entity_name} changement de facteur de puissance", "pressure": "{entity_name} modification de la pression", "signal_strength": "{entity_name} modification de la force du signal", "temperature": "{entity_name} modification de temp\u00e9rature", "timestamp": "{entity_name} modification d'horodatage", - "value": "Changements de valeur de {entity_name}" + "value": "Changements de valeur de {entity_name}", + "voltage": "{entity_name} changement de tension" } }, "state": { diff --git a/homeassistant/components/sentry/translations/fr.json b/homeassistant/components/sentry/translations/fr.json index 933bc8ad7fb..f21e0a645ea 100644 --- a/homeassistant/components/sentry/translations/fr.json +++ b/homeassistant/components/sentry/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Sentry est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Sentry est d\u00e9j\u00e0 configur\u00e9", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { "bad_dsn": "DSN invalide", @@ -26,7 +27,8 @@ "event_handled": "Envoyer des \u00e9v\u00e9nements g\u00e9r\u00e9s", "event_third_party_packages": "Envoyer des \u00e9v\u00e9nements \u00e0 partir de paquets de tiers", "logging_event_level": "Le niveau de journal de Sentry enregistrera un \u00e9v\u00e9nement pour", - "tracing": "Activer le suivi des performances" + "tracing": "Activer le suivi des performances", + "tracing_sample_rate": "Taux d'\u00e9chantillonnage de suivi ; entre 0,0 et 1,0 (1,0 = 100%)" } } } diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index 5c2d2ed5c7d..d68b9d86e75 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "auth_not_supported": "Les appareils Shelly n\u00e9cessitant une authentification ne sont actuellement pas pris en charge.", + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, "flow_title": "Shelly: {name}", "step": { + "confirm_discovery": { + "description": "Voulez-vous configurer le {model} \u00e0 {host}?" + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/smart_meter_texas/translations/fr.json b/homeassistant/components/smart_meter_texas/translations/fr.json index e059820f962..0f11528a087 100644 --- a/homeassistant/components/smart_meter_texas/translations/fr.json +++ b/homeassistant/components/smart_meter_texas/translations/fr.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, "step": { "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, "description": "Fournissez votre nom d\u2019utilisateur et votre mot de passe pour Smart Meter Texas." } } diff --git a/homeassistant/components/spotify/translations/fr.json b/homeassistant/components/spotify/translations/fr.json index d4adbe155fe..629aa5c681f 100644 --- a/homeassistant/components/spotify/translations/fr.json +++ b/homeassistant/components/spotify/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Spotify.", "authorize_url_timeout": "D\u00e9lai d'expiration g\u00e9n\u00e9rant une URL d'autorisation.", - "missing_configuration": "L'int\u00e9gration Spotify n'est pas configur\u00e9e. Veuillez suivre la documentation." + "missing_configuration": "L'int\u00e9gration Spotify n'est pas configur\u00e9e. Veuillez suivre la documentation.", + "reauth_account_mismatch": "Le compte Spotify authentifi\u00e9 ne correspond pas au compte requis pour la r\u00e9-authentification." }, "create_entry": { "default": "Authentification r\u00e9ussie avec Spotify." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "Choisissez la m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration de Spotify doit se r\u00e9-authentifier avec Spotify pour le compte: {account}", + "title": "R\u00e9-authentifier avec Spotify" } } } diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json index c6e009c00c5..c2e151e5eb7 100644 --- a/homeassistant/components/spotify/translations/no.json +++ b/homeassistant/components/spotify/translations/no.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "Du kan bare konfigurere en Spotify-konto.", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", - "missing_configuration": "Spotify-integrasjonen er ikke konfigurert. F\u00f8lg dokumentasjonen." + "missing_configuration": "Spotify-integrasjonen er ikke konfigurert. F\u00f8lg dokumentasjonen.", + "reauth_account_mismatch": "Spotify-kontoen som er autentisert med, samsvarer ikke med den kontoen som trengs re-autentisering." }, "create_entry": { "default": "Vellykket godkjenning med Spotify." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Spotify-integreringen m\u00e5 godkjennes p\u00e5 nytt med Spotify for konto: {account}", + "title": "Autentiser p\u00e5 nytt med Spotify" } } } diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index 5738707a3a4..c496fa389d9 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \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." + "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \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.", + "reauth_account_mismatch": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0443\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0435\u0439 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432 Spotify \u0434\u043b\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438: {account}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" } } } diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index 33efd72a6d8..a4fca36205f 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Spotify \u5e33\u865f\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "missing_configuration": "Spotify \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "missing_configuration": "Spotify \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "reauth_account_mismatch": "Spotify \u6240\u8a8d\u8b49\u5e33\u865f\u8207\u5e33\u865f\u4e0d\u7b26\u5408\uff0c\u9700\u91cd\u65b0\u9032\u884c\u8a8d\u8b49\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Spotify\u3002" @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Spotify \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49 Spotify \u5e33\u865f\uff1a{account}", + "title": "\u91cd\u65b0\u8a8d\u8b49 Spotify\u3002" } } } diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index 6e181e2d646..fdddb16d9bf 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "auth_failed": "Authentification non valide", + "conn_error": "\u00c9chec de connexion", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "auth_failed": "Authentification non valide" + }, "flow_title": "Configuration Tuya", "step": { "user": { "data": { - "platform": "L'application dans laquelle votre compte est enregistr\u00e9" + "country_code": "Le code de pays de votre compte (par exemple, 1 pour les \u00c9tats-Unis ou 86 pour la Chine)", + "password": "Mot de passe", + "platform": "L'application dans laquelle votre compte est enregistr\u00e9", + "username": "Nom d'utilisateur" }, "description": "Saisissez vos informations d'identification Tuya.", "title": "Tuya" diff --git a/homeassistant/components/vacuum/translations/fr.json b/homeassistant/components/vacuum/translations/fr.json index cf958c5f852..1c069a98132 100644 --- a/homeassistant/components/vacuum/translations/fr.json +++ b/homeassistant/components/vacuum/translations/fr.json @@ -19,8 +19,8 @@ "docked": "Sur la base", "error": "Erreur", "idle": "Inactif", - "off": "Off", - "on": "On", + "off": "Inactif", + "on": "Actif", "paused": "En pause", "returning": "Retourne \u00e0 la base" } diff --git a/homeassistant/components/vizio/translations/fr.json b/homeassistant/components/vizio/translations/fr.json index 914d35d58d3..9f4f55080bc 100644 --- a/homeassistant/components/vizio/translations/fr.json +++ b/homeassistant/components/vizio/translations/fr.json @@ -7,8 +7,9 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "complete_pairing_failed": "Impossible de terminer l'appairage. Assurez-vous que le code PIN que vous avez fourni est correct, que le t\u00e9l\u00e9viseur est toujours aliment\u00e9 et connect\u00e9 au r\u00e9seau avant de tenter \u00e0 nouveau.", - "host_exists": "H\u00f4te d\u00e9j\u00e0 configur\u00e9.", - "name_exists": "Nom d\u00e9j\u00e0 configur\u00e9." + "existing_config_entry_found": "Une entr\u00e9e de configuration existante Appareil VIZIO Smartcast avec le m\u00eame num\u00e9ro de s\u00e9rie a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e. Vous devez supprimer l'entr\u00e9e existante pour configurer celle-ci.", + "host_exists": "Appareil VIZIO Smartcast d\u00e9j\u00e0 configur\u00e9 avec l'h\u00f4te sp\u00e9cifi\u00e9", + "name_exists": "L'appareil VIZIO SmartCast avec le nom sp\u00e9cifi\u00e9 est d\u00e9j\u00e0 configur\u00e9." }, "step": { "pair_tv": { @@ -19,21 +20,22 @@ "title": "Processus de couplage complet" }, "pairing_complete": { - "description": "Votre appareil Vizio SmartCast est maintenant connect\u00e9 \u00e0 Home Assistant.", + "description": "Votre Appareil VIZIO Smartcast est maintenant connect\u00e9 \u00e0 Home Assistant.", "title": "Appairage termin\u00e9" }, "pairing_complete_import": { + "description": "Votre Appareil VIZIO Smartcast est maintenant connect\u00e9 \u00e0 Home Assistant.\n\nVotre Jeton d'acc\u00e8s est '**{access_token}**'.", "title": "Appairage termin\u00e9" }, "user": { "data": { "access_token": "Jeton d'acc\u00e8s", "device_class": "Type d'appareil", - "host": ":", + "host": "H\u00f4te", "name": "Nom" }, "description": "Un jeton d'acc\u00e8s n'est n\u00e9cessaire que pour les t\u00e9l\u00e9viseurs. Si vous configurez un t\u00e9l\u00e9viseur et que vous n'avez pas encore de jeton d'acc\u00e8s, laissez-le vide pour passer par un processus de couplage.", - "title": "Configurer le client Vizio SmartCast" + "title": "Appareil VIZIO Smartcast" } } }, diff --git a/homeassistant/components/wilight/translations/fr.json b/homeassistant/components/wilight/translations/fr.json index 2368c95fd88..5d54070b401 100644 --- a/homeassistant/components/wilight/translations/fr.json +++ b/homeassistant/components/wilight/translations/fr.json @@ -1,12 +1,14 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "not_supported_device": "Ce WiLight n'est actuellement pas pris en charge", "not_wilight_device": "Cet appareil n'est pas WiLight" }, "flow_title": "WiLight: {name}", "step": { "confirm": { + "description": "Voulez-vous configurer WiLight {name} ? \n\n Il prend en charge: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wled/translations/fr.json b/homeassistant/components/wled/translations/fr.json index 14f43584bc7..694627b8baf 100644 --- a/homeassistant/components/wled/translations/fr.json +++ b/homeassistant/components/wled/translations/fr.json @@ -14,6 +14,10 @@ "host": "Nom d'h\u00f4te ou adresse IP" }, "description": "Configurez votre WLED pour l'int\u00e9grer \u00e0 Home Assistant." + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter le dispositif WLED nomm\u00e9 `{name}` \u00e0 Home Assistant?", + "title": "Dispositif WLED d\u00e9couvert" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json index d0ff2a94673..a46dc756390 100644 --- a/homeassistant/components/xiaomi_aqara/translations/fr.json +++ b/homeassistant/components/xiaomi_aqara/translations/fr.json @@ -7,8 +7,10 @@ }, "error": { "discovery_error": "Impossible de d\u00e9couvrir une passerelle Xiaomi Aqara, essayez d'utiliser l'IP du p\u00e9riph\u00e9rique ex\u00e9cutant HomeAssistant comme interface", + "invalid_host": "Adresse IP non valide", "invalid_interface": "Interface r\u00e9seau non valide", "invalid_key": "Cl\u00e9 de passerelle non valide", + "invalid_mac": "Adresse MAC non valide", "not_found_error": "La passerelle d\u00e9couverte par Zeroconf ne permet pas d'obtenir les informations n\u00e9cessaires, essayez d'utiliser l'IP du p\u00e9riph\u00e9rique ex\u00e9cutant HomeAssistant comme interface" }, "flow_title": "Passerelle Xiaomi Aqara: {nom}", @@ -30,8 +32,12 @@ }, "user": { "data": { - "interface": "Interface r\u00e9seau \u00e0 utiliser" - } + "host": "Adresse IP (facultatif)", + "interface": "Interface r\u00e9seau \u00e0 utiliser", + "mac": "Adresse MAC (facultatif)" + }, + "description": "Connectez-vous \u00e0 votre passerelle Xiaomi Aqara, si les adresses IP et mac sont laiss\u00e9es vides, la d\u00e9tection automatique est utilis\u00e9e", + "title": "Passerelle Xiaomi Aqara" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index cbc890dc415..e76e5a1ef69 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -1,10 +1,11 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Le flux de configuration pour cet appareil Xiaomi Miio est d\u00e9j\u00e0 en cours." }, "error": { - "connect_error": "Impossible de se connecter, veuillez r\u00e9essayer", + "connect_error": "\u00c9chec de connexion", "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil." }, "flow_title": "Xiaomi Miio: {name}", diff --git a/homeassistant/components/zwave/translations/fr.json b/homeassistant/components/zwave/translations/fr.json index ccd5db34d3c..394670cac46 100644 --- a/homeassistant/components/zwave/translations/fr.json +++ b/homeassistant/components/zwave/translations/fr.json @@ -26,8 +26,8 @@ "sleeping": "En veille" }, "query_stage": { - "dead": "Morte ( {query_stage} )", - "initializing": "Initialisation ( {query_stage} )" + "dead": "Morte", + "initializing": "Initialisation" } } } \ No newline at end of file From 6348f130bc7956f50fb4c844e85d2ee6d3bb41bb Mon Sep 17 00:00:00 2001 From: pbalogh77 Date: Sat, 29 Aug 2020 04:16:02 +0200 Subject: [PATCH 423/862] Add basic lock support for fibaro (#38962) Added very basic support for locks in the Fibaro integration. --- homeassistant/components/fibaro/__init__.py | 4 ++ homeassistant/components/fibaro/lock.py | 47 +++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 homeassistant/components/fibaro/lock.py diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index c62dd2b2a8f..54dd5b6234f 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -44,6 +44,7 @@ FIBARO_COMPONENTS = [ "light", "scene", "sensor", + "lock", "switch", ] @@ -67,6 +68,7 @@ FIBARO_TYPEMAP = { "com.fibaro.setpoint": "climate", "com.fibaro.FGT001": "climate", "com.fibaro.thermostatDanfoss": "climate", + "com.fibaro.doorLock": "lock", } DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( @@ -220,6 +222,8 @@ class FibaroController: device_type = "switch" elif "open" in device.actions: device_type = "cover" + elif "secure" in device.actions: + device_type = "lock" elif "value" in device.properties: if device.properties.value in ("true", "false"): device_type = "binary_sensor" diff --git a/homeassistant/components/fibaro/lock.py b/homeassistant/components/fibaro/lock.py new file mode 100644 index 00000000000..a0dd60d52fe --- /dev/null +++ b/homeassistant/components/fibaro/lock.py @@ -0,0 +1,47 @@ +"""Support for Fibaro locks.""" +import logging + +from homeassistant.components.lock import DOMAIN, LockEntity + +from . import FIBARO_DEVICES, FibaroDevice + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fibaro locks.""" + if discovery_info is None: + return + + add_entities( + [FibaroLock(device) for device in hass.data[FIBARO_DEVICES]["lock"]], True + ) + + +class FibaroLock(FibaroDevice, LockEntity): + """Representation of a Fibaro Lock.""" + + def __init__(self, fibaro_device): + """Initialize the Fibaro device.""" + self._state = False + super().__init__(fibaro_device) + self.entity_id = f"{DOMAIN}.{self.ha_id}" + + def lock(self, **kwargs): + """Lock the device.""" + self.action("secure") + self._state = True + + def unlock(self, **kwargs): + """Unlock the device.""" + self.action("unsecure") + self._state = False + + @property + def is_locked(self): + """Return true if device is locked.""" + return self._state + + def update(self): + """Update device state.""" + self._state = self.current_binary_state From 4682de5ac109ffad0bfbd463cd7ce166405fa68c Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 28 Aug 2020 21:26:19 -0500 Subject: [PATCH 424/862] Improve patching in broadlink sensor tests (#39366) * improve patching in broadlink sensor tests * Update test_sensors.py * Update test_sensors.py --- tests/components/broadlink/test_sensors.py | 24 ++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index 8d1f7be9e5d..132ab31d8be 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -8,6 +8,14 @@ from tests.async_mock import patch from tests.common import mock_device_registry, mock_registry +def _patch_broadlink_gendevice(return_value): + """Patch the broadlink gendevice method.""" + return patch( + "homeassistant.components.broadlink.device.blk.gendevice", + return_value=return_value, + ) + + async def test_a1_sensor_setup(hass): """Test a successful e-Sensor setup.""" device = get_device("Bedroom") @@ -25,7 +33,7 @@ async def test_a1_sensor_setup(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with patch("broadlink.gendevice", return_value=mock_api): + with _patch_broadlink_gendevice(return_value=mock_api): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -67,7 +75,7 @@ async def test_a1_sensor_update(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with patch("broadlink.gendevice", return_value=mock_api): + with _patch_broadlink_gendevice(return_value=mock_api): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -114,7 +122,7 @@ async def test_rm_pro_sensor_setup(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with patch("broadlink.gendevice", return_value=mock_api): + with _patch_broadlink_gendevice(return_value=mock_api): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -144,7 +152,7 @@ async def test_rm_pro_sensor_update(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with patch("broadlink.gendevice", return_value=mock_api): + with _patch_broadlink_gendevice(return_value=mock_api): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -179,7 +187,7 @@ async def test_rm_mini3_no_sensor(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with patch("broadlink.gendevice", return_value=mock_api): + with _patch_broadlink_gendevice(return_value=mock_api): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -203,7 +211,7 @@ async def test_rm4_pro_hts2_sensor_setup(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with patch("broadlink.gendevice", return_value=mock_api): + with _patch_broadlink_gendevice(return_value=mock_api): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -236,7 +244,7 @@ async def test_rm4_pro_hts2_sensor_update(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with patch("broadlink.gendevice", return_value=mock_api): + with _patch_broadlink_gendevice(return_value=mock_api): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -274,7 +282,7 @@ async def test_rm4_pro_no_sensor(hass): device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with patch("broadlink.gendevice", return_value=mock_api): + with _patch_broadlink_gendevice(return_value=mock_api): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() From 67de1d346631d37c1bf7c8e5ee71aebb9a38b2ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2020 22:06:06 -0500 Subject: [PATCH 425/862] Fix sun test to patch time since it is now refetched (#39372) This accounts for the fix in #39335 --- tests/components/sun/test_init.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 56ac683582a..36eaa8398ac 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -120,11 +120,12 @@ async def test_state_change(hass, legacy_patchable_time): assert sun.STATE_BELOW_HORIZON == hass.states.get(sun.ENTITY_ID).state - hass.bus.async_fire( - ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: test_time + timedelta(seconds=5)} - ) - - await hass.async_block_till_done() + patched_time = test_time + timedelta(seconds=5) + with patch( + "homeassistant.helpers.condition.dt_util.utcnow", return_value=patched_time + ): + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: patched_time}) + await hass.async_block_till_done() assert sun.STATE_ABOVE_HORIZON == hass.states.get(sun.ENTITY_ID).state From 1bf2c4d976c73841f9c4ba94a4da419dbe3ba4b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 29 Aug 2020 08:59:24 +0300 Subject: [PATCH 426/862] Upgrade pylint to 2.6.0 (#39363) --- .travis.yml | 3 ++- homeassistant/auth/permissions/__init__.py | 2 +- .../components/acmeda/config_flow.py | 2 -- .../components/almond/config_flow.py | 6 ++--- .../components/androidtv/media_player.py | 2 +- .../components/apache_kafka/__init__.py | 2 +- homeassistant/components/apple_tv/__init__.py | 2 +- homeassistant/components/avea/light.py | 2 +- .../components/beewi_smartclim/sensor.py | 2 +- .../components/conversation/__init__.py | 2 +- homeassistant/components/decora/light.py | 4 ++- .../components/dialogflow/__init__.py | 1 - .../components/eq3btsmart/climate.py | 3 +-- homeassistant/components/esphome/__init__.py | 1 - homeassistant/components/flume/config_flow.py | 2 +- homeassistant/components/geofency/__init__.py | 1 - .../components/google_assistant/trait.py | 2 +- .../components/google_pubsub/__init__.py | 2 +- .../components/gpslogger/__init__.py | 1 - homeassistant/components/habitica/__init__.py | 2 +- homeassistant/components/hdmi_cec/__init__.py | 14 ++++++---- .../components/hdmi_cec/media_player.py | 8 ++++-- homeassistant/components/homekit/__init__.py | 1 - homeassistant/components/ifttt/__init__.py | 1 - .../components/itunes/media_player.py | 2 +- homeassistant/components/knx/light.py | 1 - homeassistant/components/light/__init__.py | 1 - .../components/limitlessled/light.py | 2 +- homeassistant/components/locative/__init__.py | 1 - .../components/lutron_caseta/binary_sensor.py | 2 +- homeassistant/components/mailgun/__init__.py | 1 - homeassistant/components/miflora/sensor.py | 2 +- homeassistant/components/mikrotik/hub.py | 8 ++++-- .../components/nuheat/config_flow.py | 2 +- homeassistant/components/plex/server.py | 2 +- .../components/simplisafe/__init__.py | 2 +- .../components/system_log/__init__.py | 2 +- homeassistant/components/traccar/__init__.py | 1 - homeassistant/components/twilio/__init__.py | 1 - .../components/websocket_api/__init__.py | 2 -- .../components/zha/core/registries.py | 7 ++--- homeassistant/components/zwave/__init__.py | 26 +++++++++---------- homeassistant/config_entries.py | 1 + homeassistant/core.py | 5 ++-- homeassistant/helpers/event.py | 3 --- homeassistant/helpers/json.py | 1 - homeassistant/helpers/typing.py | 2 -- homeassistant/runner.py | 6 ++--- homeassistant/util/async_.py | 2 +- homeassistant/util/logging.py | 2 +- pylintrc | 4 ++- requirements_test.txt | 4 +-- 52 files changed, 77 insertions(+), 86 deletions(-) diff --git a/.travis.yml b/.travis.yml index bcf4f7e1cb9..88bc8b707cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,8 @@ jobs: - python: "3.7.1" env: TOXENV=lint - python: "3.7.1" - env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30 + # PYLINT_ARGS=--jobs=0 disabled for now: https://github.com/PyCQA/pylint/issues/3584 + env: TOXENV=pylint TRAVIS_WAIT=30 - python: "3.7.1" env: TOXENV=typing diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 92d02c75b91..2f887d21b02 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -72,4 +72,4 @@ class _OwnerPermissions(AbstractPermissions): return lambda entity_id, key: True -OwnerPermissions = _OwnerPermissions() # pylint: disable=invalid-name +OwnerPermissions = _OwnerPermissions() diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py index 33dac4814e5..816768b0800 100644 --- a/homeassistant/components/acmeda/config_flow.py +++ b/homeassistant/components/acmeda/config_flow.py @@ -26,10 +26,8 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if ( user_input is not None and self.discovered_hubs is not None - # pylint: disable=unsupported-membership-test and user_input["id"] in self.discovered_hubs ): - # pylint: disable=unsubscriptable-object return await self.async_create(self.discovered_hubs[user_input["id"]]) # Already configured hosts diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index 73dc85c5fd0..0421612b702 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -11,7 +11,7 @@ from yarl import URL from homeassistant import config_entries, core, data_entry_flow from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow -from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 +from .const import DOMAIN as ALMOND_DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 async def async_verify_local_connection(hass: core.HomeAssistant, host: str): @@ -28,11 +28,11 @@ async def async_verify_local_connection(hass: core.HomeAssistant, host: str): return False -@config_entries.HANDLERS.register(DOMAIN) +@config_entries.HANDLERS.register(ALMOND_DOMAIN) class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): """Implementation of the Almond OAuth2 config flow.""" - DOMAIN = DOMAIN + DOMAIN = ALMOND_DOMAIN host = None hassio_discovery = None diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 0d39d528cca..a46685a1c65 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -374,7 +374,7 @@ def adb_decorator(override_available=False): err, ) await self.aftv.adb_close() - self._available = False # pylint: disable=protected-access + self._available = False return None return _adb_exception_catcher diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index 7bd23630bd0..c64e2159977 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -64,7 +64,7 @@ class DateTimeJSONEncoder(json.JSONEncoder): Additionally add encoding for datetime objects as isoformat. """ - def default(self, o): # pylint: disable=method-hidden + def default(self, o): """Implement encoding logic.""" if isinstance(o, datetime): return o.isoformat() diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index aae4165fe5f..80c09606dbf 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -39,7 +39,7 @@ NOTIFICATION_AUTH_TITLE = "Apple TV Authentication" NOTIFICATION_SCAN_ID = "apple_tv_scan_notification" NOTIFICATION_SCAN_TITLE = "Apple TV Scan" -T = TypeVar("T") # pylint: disable=invalid-name +T = TypeVar("T") # This version of ensure_list interprets an empty dict as no value diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index 8f57fb08e96..c3f6e19c2e2 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -1,7 +1,7 @@ """Support for the Elgato Avea lights.""" import logging -import avea +import avea # pylint: disable=import-error from homeassistant.components.light import ( ATTR_BRIGHTNESS, diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index ec124b24971..b4dcc289a7b 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -1,7 +1,7 @@ """Platform for beewi_smartclim integration.""" import logging -from beewi_smartclim import BeewiSmartClimPoller +from beewi_smartclim import BeewiSmartClimPoller # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index dd17eca6792..f8534d99935 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -41,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -async_register = bind_hass(async_register) # pylint: disable=invalid-name +async_register = bind_hass(async_register) @core.callback diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index b9bf09f0c6d..4335c99076a 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -4,7 +4,9 @@ from functools import wraps import logging import time -from bluepy.btle import BTLEException # pylint: disable=import-error, no-member +from bluepy.btle import ( # pylint: disable=import-error, no-member, no-name-in-module + BTLEException, +) import decora # pylint: disable=import-error, no-member import voluptuous as vol diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index ae3c0288aed..b9ba977e422 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -80,7 +80,6 @@ async def async_unload_entry(hass, entry): return True -# pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 402dfc684b3..6eb0276c314 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -1,8 +1,7 @@ """Support for eQ-3 Bluetooth Smart thermostats.""" import logging -# pylint: disable=import-error -from bluepy.btle import BTLEException +from bluepy.btle import BTLEException # pylint: disable=import-error, no-name-in-module import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index cdc9074c5ab..4248366ffad 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -433,7 +433,6 @@ def esphome_state_property(func): @property def _wrapper(self): - # pylint: disable=protected-access if self._state is None: return None val = func(self) diff --git a/homeassistant/components/flume/config_flow.py b/homeassistant/components/flume/config_flow.py index 80f1698a7e6..e0e5bf8efe9 100644 --- a/homeassistant/components/flume/config_flow.py +++ b/homeassistant/components/flume/config_flow.py @@ -59,7 +59,7 @@ async def validate_input(hass: core.HomeAssistant, data): flume_devices = await hass.async_add_executor_job(FlumeDeviceList, flume_auth) except RequestException as err: raise CannotConnect from err - except Exception as err: # pylint: disable=broad-except + except Exception as err: raise InvalidAuth from err if not flume_devices or not flume_devices.device_list: raise CannotConnect diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index cb663676512..50a7d8ac577 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -154,5 +154,4 @@ async def async_unload_entry(hass, entry): return True -# pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index f7aa2d43663..569b12e4730 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1715,7 +1715,7 @@ class VolumeTrait(_Trait): svc = media_player.SERVICE_VOLUME_DOWN relative = -relative - for i in range(relative): + for _ in range(relative): await self.hass.services.async_call( media_player.DOMAIN, svc, diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index bc7811a7a8f..8d7a675860c 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -89,7 +89,7 @@ class DateTimeJSONEncoder(json.JSONEncoder): Additionally add encoding for datetime objects as isoformat. """ - def default(self, o): # pylint: disable=method-hidden + def default(self, o): """Implement encoding logic.""" if isinstance(o, datetime.datetime): return o.isoformat() diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index aa95d17cbfc..f09a7f86d2a 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -116,5 +116,4 @@ async def async_unload_entry(hass, entry): return True -# pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index ac504821b35..b2c3fb16831 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -49,7 +49,7 @@ INSTANCE_SCHEMA = vol.Schema( } ) -has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name +has_unique_values = vol.Schema(vol.Unique()) # because we want a handy alias diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index c9a5d27a3be..c272ad19c8d 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -4,9 +4,13 @@ from functools import reduce import logging import multiprocessing -from pycec.cec import CecAdapter -from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand -from pycec.const import ( +from pycec.cec import CecAdapter # pylint: disable=import-error +from pycec.commands import ( # pylint: disable=import-error + CecCommand, + KeyPressCommand, + KeyReleaseCommand, +) +from pycec.const import ( # pylint: disable=import-error ADDR_AUDIOSYSTEM, ADDR_BROADCAST, ADDR_UNREGISTERED, @@ -21,8 +25,8 @@ from pycec.const import ( STATUS_STILL, STATUS_STOP, ) -from pycec.network import HDMINetwork, PhysicalAddress -from pycec.tcp import TcpAdapter +from pycec.network import HDMINetwork, PhysicalAddress # pylint: disable=import-error +from pycec.tcp import TcpAdapter # pylint: disable=import-error import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index c3cab6a8f98..f81ee20afe3 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -1,8 +1,12 @@ """Support for HDMI CEC devices as media players.""" import logging -from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand -from pycec.const import ( +from pycec.commands import ( # pylint: disable=import-error + CecCommand, + KeyPressCommand, + KeyReleaseCommand, +) +from pycec.const import ( # pylint: disable=import-error KEY_BACKWARD, KEY_FORWARD, KEY_MUTE_TOGGLE, diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index e8360b1d73b..f72cb082671 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -723,7 +723,6 @@ class HomeKitPairingQRView(HomeAssistantView): name = "api:homekit:pairingqr" requires_auth = False - # pylint: disable=no-self-use async def get(self, request): """Retrieve the pairing QRCode image.""" if not request.query_string: diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 4c579033fff..9e77e49709c 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -123,5 +123,4 @@ async def async_unload_entry(hass, entry): return True -# pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index 707cac6f953..a62930ff9dc 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -388,7 +388,7 @@ class ItunesDevice(MediaPlayerEntity): def media_next_track(self): """Send media_next command to media player.""" - response = self.client.next() + response = self.client.next() # pylint: disable=not-callable self.update_state(response) def media_previous_track(self): diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index ac0bf2122a8..2fc53fa3b7c 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -94,7 +94,6 @@ class KNXLight(LightEntity): return self.device.current_brightness hsv_color = self._hsv_color if self.device.supports_color and hsv_color: - # pylint: disable=unsubscriptable-object return round(hsv_color[-1] / 100 * 255) return None diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b97e9e90248..723967b3d51 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -442,7 +442,6 @@ class LightEntity(ToggleEntity): data[ATTR_COLOR_TEMP] = self.color_temp if supported_features & SUPPORT_COLOR and self.hs_color: - # pylint: disable=unsubscriptable-object,not-an-iterable hs_color = self.hs_color data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 682d619a92c..6dbaf0c3b7c 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -176,7 +176,7 @@ def state(new_state): def decorator(function): """Set up the decorator function.""" - # pylint: disable=protected-access + def wrapper(self, **kwargs): """Wrap a group state change.""" diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 978f50b0ffd..28af822ae63 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -128,5 +128,4 @@ async def async_unload_entry(hass, entry): return await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) -# pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index b517dda1ba3..cb50cb1a6e8 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -64,7 +64,7 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): sensors by each room. Therefore, there shouldn't be devices related to any sensor entities. """ - return None # pylint: disable=useless-return + return None @property def device_state_attributes(self): diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index 57c83d8c20c..42b51c23c74 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -95,5 +95,4 @@ async def async_unload_entry(hass, entry): return True -# pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 8ed4d02ea1d..3b5d7166425 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -5,7 +5,7 @@ import logging import btlewrap from btlewrap import BluetoothBackendException -from miflora import miflora_poller +from miflora import miflora_poller # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index fb120aa29c7..4136377fde9 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -129,7 +129,11 @@ class MikrotikData: """Return device model name.""" cmd = IDENTITY if param == NAME else INFO data = self.command(MIKROTIK_SERVICES[cmd]) - return data[0].get(param) if data else None + return ( + data[0].get(param) # pylint: disable=unsubscriptable-object + if data + else None + ) def get_hub_details(self): """Get Hub info.""" @@ -229,7 +233,7 @@ class MikrotikData: data = self.command(cmd, params) if data is not None: status = 0 - for result in data: + for result in data: # pylint: disable=not-an-iterable if "status" in result: status += 1 if status == len(data): diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py index dc72438ce5e..eb76c620767 100644 --- a/homeassistant/components/nuheat/config_flow.py +++ b/homeassistant/components/nuheat/config_flow.py @@ -48,7 +48,7 @@ async def validate_input(hass: core.HomeAssistant, data): # # The underlying module throws a generic exception on login failure # - except Exception as ex: # pylint: disable=broad-except + except Exception as ex: raise InvalidAuth from ex try: diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 2ddb5ef0a29..1d237dedb01 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -184,7 +184,7 @@ class PlexServer: if _update_plexdirect_hostname(): config_entry_update_needed = True else: - raise Unauthorized( + raise Unauthorized( # pylint: disable=raise-missing-from "New certificate cannot be validated with provided token" ) else: diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 4b843d6eebe..0a42d42be3a 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -566,7 +566,7 @@ class SimpliSafe: LOGGER.error("SimpliSafe error while updating: %s", result) return - if isinstance(result, Exception): # pylint: disable=broad-except + if isinstance(result, Exception): LOGGER.error("Unknown error while updating: %s", result) return diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 6f658962fe0..b8d6b1664ac 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -165,7 +165,7 @@ class LogErrorQueueHandler(logging.handlers.QueueHandler): """Emit a log record.""" try: self.enqueue(record) - except asyncio.CancelledError: # pylint: disable=try-except-raise + except asyncio.CancelledError: raise except Exception: # pylint: disable=broad-except self.handleError(record) diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 03482292135..5203ea5ec90 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -107,5 +107,4 @@ async def async_unload_entry(hass, entry): return True -# pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index 15c6697b2f7..7c65a693af1 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -64,5 +64,4 @@ async def async_unload_entry(hass, entry): return True -# pylint: disable=invalid-name async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 60177fcde90..bfcfc796bad 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -15,7 +15,6 @@ DOMAIN = const.DOMAIN DEPENDENCIES = ("http",) # Backwards compat / Make it easier to integrate -# pylint: disable=invalid-name ActiveConnection = connection.ActiveConnection BASE_COMMAND_MESSAGE_SCHEMA = messages.BASE_COMMAND_MESSAGE_SCHEMA error_message = messages.error_message @@ -25,7 +24,6 @@ async_response = decorators.async_response require_admin = decorators.require_admin ws_require_user = decorators.ws_require_user websocket_command = decorators.websocket_command -# pylint: enable=invalid-name @bind_hass diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 0df3b1070bf..0cc350ba4b8 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -240,12 +240,9 @@ class MatchRule: return matches -RegistryDictType = Dict[ - str, Dict[MatchRule, CALLABLE_T] -] # pylint: disable=invalid-name +RegistryDictType = Dict[str, Dict[MatchRule, CALLABLE_T]] - -GroupRegistryDictType = Dict[str, CALLABLE_T] # pylint: disable=invalid-name +GroupRegistryDictType = Dict[str, CALLABLE_T] class ZHAEntityRegistry: diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 4e0598fed95..296271288d2 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -593,7 +593,7 @@ async def async_setup_entry(hass, config_entry): async def rename_node(service): """Rename a node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object name = service.data.get(const.ATTR_NAME) node.name = name _LOGGER.info("Renamed Z-Wave node %d to %s", node_id, name) @@ -613,7 +613,7 @@ async def async_setup_entry(hass, config_entry): """Rename a node value.""" node_id = service.data.get(const.ATTR_NODE_ID) value_id = service.data.get(const.ATTR_VALUE_ID) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object value = node.values[value_id] name = service.data.get(const.ATTR_NAME) value.label = name @@ -629,7 +629,7 @@ async def async_setup_entry(hass, config_entry): """Set the polling intensity of a node value.""" node_id = service.data.get(const.ATTR_NODE_ID) value_id = service.data.get(const.ATTR_VALUE_ID) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object value = node.values[value_id] intensity = service.data.get(const.ATTR_POLL_INTENSITY) if intensity == 0: @@ -667,7 +667,7 @@ async def async_setup_entry(hass, config_entry): def set_config_parameter(service): """Set a config parameter to a node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object param = service.data.get(const.ATTR_CONFIG_PARAMETER) selection = service.data.get(const.ATTR_CONFIG_VALUE) size = service.data.get(const.ATTR_CONFIG_SIZE) @@ -725,7 +725,7 @@ async def async_setup_entry(hass, config_entry): """Refresh the specified value from a node.""" node_id = service.data.get(const.ATTR_NODE_ID) value_id = service.data.get(const.ATTR_VALUE_ID) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object node.values[value_id].refresh() _LOGGER.info("Node %s value %s refreshed", node_id, value_id) @@ -734,14 +734,14 @@ async def async_setup_entry(hass, config_entry): node_id = service.data.get(const.ATTR_NODE_ID) value_id = service.data.get(const.ATTR_VALUE_ID) value = service.data.get(const.ATTR_CONFIG_VALUE) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object node.values[value_id].data = value _LOGGER.info("Node %s value %s set to %s", node_id, value_id, value) def print_config_parameter(service): """Print a config parameter from a node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object param = service.data.get(const.ATTR_CONFIG_PARAMETER) _LOGGER.info( "Config parameter %s on Node %s: %s", @@ -753,13 +753,13 @@ async def async_setup_entry(hass, config_entry): def print_node(service): """Print all information about z-wave node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object nice_print_node(node) def set_wakeup(service): """Set wake-up interval of a node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object value = service.data.get(const.ATTR_CONFIG_VALUE) if node.can_wake_up(): for value_id in node.get_values(class_id=const.COMMAND_CLASS_WAKE_UP): @@ -806,14 +806,14 @@ async def async_setup_entry(hass, config_entry): def refresh_node(service): """Refresh all node info.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object node.refresh_info() def reset_node_meters(service): """Reset meter counters of a node.""" node_id = service.data.get(const.ATTR_NODE_ID) instance = service.data.get(const.ATTR_INSTANCE) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object for value in node.get_values(class_id=const.COMMAND_CLASS_METER).values(): if value.index != const.INDEX_METER_RESET: continue @@ -833,7 +833,7 @@ async def async_setup_entry(hass, config_entry): """Heal a node on the network.""" node_id = service.data.get(const.ATTR_NODE_ID) update_return_routes = service.data.get(const.ATTR_RETURN_ROUTES) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object _LOGGER.info("Z-Wave node heal running for node %s", node_id) node.heal(update_return_routes) @@ -841,7 +841,7 @@ async def async_setup_entry(hass, config_entry): """Send test messages to a node on the network.""" node_id = service.data.get(const.ATTR_NODE_ID) messages = service.data.get(const.ATTR_MESSAGES) - node = network.nodes[node_id] + node = network.nodes[node_id] # pylint: disable=unsubscriptable-object _LOGGER.info("Sending %s test-messages to node %s", messages, node_id) node.test(messages) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f708f138f88..347ca294d34 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -743,6 +743,7 @@ class ConfigEntries: self, entry: ConfigEntry, *, + # pylint: disable=dangerous-default-value # _UNDEFs not modified unique_id: Union[str, dict, None] = _UNDEF, title: Union[str, dict] = _UNDEF, data: dict = _UNDEF, diff --git a/homeassistant/core.py b/homeassistant/core.py index 6b44386eadf..1540b69b36b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -158,7 +158,7 @@ class CoreState(enum.Enum): final_write = "FINAL_WRITE" stopped = "STOPPED" - def __str__(self) -> str: + def __str__(self) -> str: # pylint: disable=invalid-str-returned """Return the event.""" return self.value # type: ignore @@ -523,7 +523,7 @@ class EventOrigin(enum.Enum): local = "LOCAL" remote = "REMOTE" - def __str__(self) -> str: + def __str__(self) -> str: # pylint: disable=invalid-str-returned """Return the event.""" return self.value # type: ignore @@ -1483,6 +1483,7 @@ class Config: unit_system: Optional[str] = None, location_name: Optional[str] = None, time_zone: Optional[str] = None, + # pylint: disable=dangerous-default-value # _UNDEFs not modified external_url: Optional[Union[str, dict]] = _UNDEF, internal_url: Optional[Union[str, dict]] = _UNDEF, ) -> None: diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 21f49c008c8..0530ad7cdc7 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -47,9 +47,6 @@ TRACK_ENTITY_REGISTRY_UPDATED_LISTENER = "track_entity_registry_updated_listener _LOGGER = logging.getLogger(__name__) -# PyLint does not like the use of threaded_listener_factory -# pylint: disable=invalid-name - def threaded_listener_factory(async_factory: Callable[..., Any]) -> CALLBACK_TYPE: """Convert an async event helper to a threaded one.""" diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 025cac11641..4acb9fd500a 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -10,7 +10,6 @@ _LOGGER = logging.getLogger(__name__) class JSONEncoder(json.JSONEncoder): """JSONEncoder that supports Home Assistant objects.""" - # pylint: disable=method-hidden def default(self, o: Any) -> Any: """Convert Home Assistant objects. diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 0be267e3a86..6bcc98c10a8 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -3,8 +3,6 @@ from typing import Any, Dict, Optional, Tuple, Union import homeassistant.core -# pylint: disable=invalid-name - GPSType = Tuple[float, float] ConfigType = Dict[str, Any] ContextType = homeassistant.core.Context diff --git a/homeassistant/runner.py b/homeassistant/runner.py index f35de991a27..a5cf0f88a40 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -46,7 +46,7 @@ class RuntimeConfig: if sys.platform == "win32" and sys.version_info[:2] < (3, 8): PolicyBase = asyncio.WindowsProactorEventLoopPolicy else: - PolicyBase = asyncio.DefaultEventLoopPolicy # pylint: disable=invalid-name + PolicyBase = asyncio.DefaultEventLoopPolicy class HassEventLoopPolicy(PolicyBase): # type: ignore @@ -117,9 +117,7 @@ def _async_loop_exception_handler(_: Any, context: Dict) -> None: ) -async def setup_and_run_hass( - runtime_config: RuntimeConfig, -) -> int: +async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: """Set up Home Assistant and run.""" hass = await bootstrap.async_setup_hass(runtime_config) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index a33f548a8a9..0b48e2159c0 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -10,7 +10,7 @@ from typing import Any, Callable, Coroutine, TypeVar _LOGGER = logging.getLogger(__name__) -T = TypeVar("T") # pylint: disable=invalid-name +T = TypeVar("T") def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index ed710f573f4..629928f43d7 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -34,7 +34,7 @@ class HomeAssistantQueueHandler(logging.handlers.QueueHandler): """Emit a log record.""" try: self.enqueue(record) - except asyncio.CancelledError: # pylint: disable=try-except-raise + except asyncio.CancelledError: raise except Exception: # pylint: disable=broad-except self.handleError(record) diff --git a/pylintrc b/pylintrc index f2860026cd8..86f3d4caea0 100644 --- a/pylintrc +++ b/pylintrc @@ -2,7 +2,8 @@ ignore=tests # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. -jobs=2 +# Disabled for now: https://github.com/PyCQA/pylint/issues/3584 +#jobs=2 load-plugins=pylint_strict_informational persistent=no extension-pkg-whitelist=ciso8601,cv2 @@ -46,6 +47,7 @@ disable= too-many-boolean-expressions, unused-argument, wrong-import-order +# enable useless-suppression temporarily every now and then to clean them up enable= use-symbolic-message-instead diff --git a/requirements_test.txt b/requirements_test.txt index 094ff1a00c6..600916615be 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,8 +10,8 @@ coverage==5.2.1 mock-open==1.4.0 mypy==0.780 pre-commit==2.7.1 -pylint==2.4.4 -astroid==2.3.3 +pylint==2.6.0 +astroid==2.4.2 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.10.0 From 6ae93992371de887255f542dee24bad09e938a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 29 Aug 2020 09:23:55 +0300 Subject: [PATCH 427/862] Upgrade isort to 5.4.2 (#37939) --- .pre-commit-config.yaml | 4 ++-- homeassistant/__main__.py | 2 +- homeassistant/auth/permissions/models.py | 6 ++++-- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/components/huawei_lte/config_flow.py | 3 ++- homeassistant/components/switcher_kis/switch.py | 2 +- .../components/tensorflow/image_processing.py | 2 +- homeassistant/components/zha/core/typing.py | 2 +- homeassistant/components/zwave/__init__.py | 9 +++++---- homeassistant/components/zwave/config_flow.py | 2 +- homeassistant/core.py | 2 +- requirements_test_pre_commit.txt | 2 +- script/gen_requirements_all.py | 3 +-- script/hassfest/translations.py | 2 +- setup.cfg | 12 +----------- tests/async_mock.py | 3 ++- tests/components/dsmr/test_sensor.py | 4 ++-- tests/components/mobile_app/test_http_api.py | 2 +- tests/components/mobile_app/test_webhook.py | 4 ++-- tests/components/owntracks/test_device_tracker.py | 6 +++--- tests/components/rflink/test_init.py | 1 + tests/components/switcher_kis/test_init.py | 3 ++- tests/hassfest/test_dependencies.py | 1 + 23 files changed, 38 insertions(+), 41 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29365969725..de54a5728d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,8 +38,8 @@ repos: - --format=custom - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - - repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 + - repo: https://github.com/timothycrosley/isort + rev: 5.4.2 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index c229409a8d3..840e0bed24d 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -195,7 +195,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None: get rid of. """ # pylint: disable=import-outside-toplevel - from fcntl import fcntl, F_GETFD, F_SETFD, FD_CLOEXEC + from fcntl import F_GETFD, F_SETFD, FD_CLOEXEC, fcntl for _fd in range(min_fd, max_fd): try: diff --git a/homeassistant/auth/permissions/models.py b/homeassistant/auth/permissions/models.py index 435d5f2e982..2542be14cc6 100644 --- a/homeassistant/auth/permissions/models.py +++ b/homeassistant/auth/permissions/models.py @@ -5,8 +5,10 @@ import attr if TYPE_CHECKING: # pylint: disable=unused-import - from homeassistant.helpers import entity_registry as ent_reg # noqa: F401 - from homeassistant.helpers import device_registry as dev_reg # noqa: F401 + from homeassistant.helpers import ( # noqa: F401 + device_registry as dev_reg, + entity_registry as ent_reg, + ) @attr.s(slots=True) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index f72cb082671..e77d4ef134e 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -606,6 +606,7 @@ class HomeKit: type_cameras, type_covers, type_fans, + type_humidifiers, type_lights, type_locks, type_media_players, @@ -613,7 +614,6 @@ class HomeKit: type_sensors, type_switches, type_thermostats, - type_humidifiers, ) for state in bridged_states: diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index b834f4dab94..9d5ce40074d 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -30,8 +30,9 @@ from homeassistant.const import ( ) from homeassistant.core import callback -# see https://github.com/PyCQA/pylint/issues/3202 about the DOMAIN's pylint issue from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME + +# see https://github.com/PyCQA/pylint/issues/3202 about the DOMAIN's pylint issue from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index c2254968901..badab64391e 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -27,8 +27,8 @@ from . import ( # pylint: disable=ungrouped-imports if TYPE_CHECKING: - from aioswitcher.devices import SwitcherV2Device from aioswitcher.api.messages import SwitcherV2ControlResponseMSG + from aioswitcher.devices import SwitcherV2Device _LOGGER = getLogger(__name__) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index d293a19d8d6..564dd7377b1 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -125,8 +125,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # (The model_dir is created during the manual setup process. See integration docs.) # pylint: disable=import-outside-toplevel - from object_detection.utils import config_util, label_map_util from object_detection.builders import model_builder + from object_detection.utils import config_util, label_map_util except ImportError: _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " diff --git a/homeassistant/components/zha/core/typing.py b/homeassistant/components/zha/core/typing.py index bce4a058ac6..0fe46a4628e 100644 --- a/homeassistant/components/zha/core/typing.py +++ b/homeassistant/components/zha/core/typing.py @@ -26,13 +26,13 @@ ZigpyGroupType = zigpy.group.Group ZigpyZdoType = zigpy.zdo.ZDO if TYPE_CHECKING: + import homeassistant.components.zha.core.channels import homeassistant.components.zha.core.channels as channels import homeassistant.components.zha.core.channels.base as base_channels import homeassistant.components.zha.core.device import homeassistant.components.zha.core.gateway import homeassistant.components.zha.core.group import homeassistant.components.zha.entity - import homeassistant.components.zha.core.channels ChannelType = base_channels.ZigbeeChannel ChannelsType = channels.Channels diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 296271288d2..d679de7cfbd 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -335,12 +335,13 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ - from pydispatch import dispatcher - # pylint: disable=import-error - from openzwave.option import ZWaveOption - from openzwave.network import ZWaveNetwork from openzwave.group import ZWaveGroup + from openzwave.network import ZWaveNetwork + from openzwave.option import ZWaveOption + + # pylint: enable=import-error + from pydispatch import dispatcher # Merge config entry and yaml config config = config_entry.data diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py index cff197b7e97..d6c64e914f6 100644 --- a/homeassistant/components/zwave/config_flow.py +++ b/homeassistant/components/zwave/config_flow.py @@ -43,8 +43,8 @@ class ZwaveFlowHandler(config_entries.ConfigFlow): if user_input is not None: # Check if USB path is valid - from openzwave.option import ZWaveOption from openzwave.object import ZWaveException + from openzwave.option import ZWaveOption try: from functools import partial diff --git a/homeassistant/core.py b/homeassistant/core.py index 1540b69b36b..ad083f60574 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -80,8 +80,8 @@ import homeassistant.util.uuid as uuid_util # Typing imports that create a circular dependency if TYPE_CHECKING: from homeassistant.auth import AuthManager - from homeassistant.config_entries import ConfigEntries from homeassistant.components.http import HomeAssistantHTTP + from homeassistant.config_entries import ConfigEntries block_async_io.enable() diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index ceca5145010..1594fa94990 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -5,7 +5,7 @@ black==20.8b1 codespell==1.17.1 flake8-docstrings==1.5.0 flake8==3.8.3 -isort==4.3.21 +isort==5.4.2 pydocstyle==5.0.2 pyupgrade==2.7.2 yamllint==1.24.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e86953a6280..27482a0c215 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,9 +8,8 @@ import pkgutil import re import sys -from script.hassfest.model import Integration - from homeassistant.util.yaml.loader import load_yaml +from script.hassfest.model import Integration COMMENT_REQUIREMENTS = ( "Adafruit_BBIO", diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 801a5112118..7a9208da7d1 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -6,12 +6,12 @@ import logging import re from typing import Dict -from script.translations import upload import voluptuous as vol from voluptuous.humanize import humanize_error import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify +from script.translations import upload from .model import Config, Integration diff --git a/setup.cfg b/setup.cfg index 6dace4932db..8dd3e083a8b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,19 +38,9 @@ ignore = [isort] # https://github.com/timothycrosley/isort # https://github.com/timothycrosley/isort/wiki/isort-Settings -# splits long import on multiple lines indented by 4 spaces -multi_line_output = 3 -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 -indent = " " -# by default isort don't check module indexes -not_skip = __init__.py +profile = black # will group `import x` and `from x import` of the same module. force_sort_within_sections = true -sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -default_section = THIRDPARTY known_first_party = homeassistant,tests forced_separate = tests combine_as_imports = true diff --git a/tests/async_mock.py b/tests/async_mock.py index 1942b2ca284..8257ddd3b3b 100644 --- a/tests/async_mock.py +++ b/tests/async_mock.py @@ -3,6 +3,7 @@ import sys if sys.version_info[:2] < (3, 8): from asynctest.mock import * # noqa - from asynctest.mock import CoroutineMock as AsyncMock # noqa + + AsyncMock = CoroutineMock # noqa: F405 else: from unittest.mock import * # noqa diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ed660eb4f51..95608aedba7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -153,8 +153,8 @@ async def test_v4_meter(hass, mock_connection_factory): (connection_factory, transport, protocol) = mock_connection_factory from dsmr_parser.obis_references import ( - HOURLY_GAS_METER_READING, ELECTRICITY_ACTIVE_TARIFF, + HOURLY_GAS_METER_READING, ) from dsmr_parser.objects import CosemObject, MBusObject @@ -198,8 +198,8 @@ async def test_v5_meter(hass, mock_connection_factory): (connection_factory, transport, protocol) = mock_connection_factory from dsmr_parser.obis_references import ( - HOURLY_GAS_METER_READING, ELECTRICITY_ACTIVE_TARIFF, + HOURLY_GAS_METER_READING, ) from dsmr_parser.objects import CosemObject, MBusObject diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 6cb08580005..c5b363c25b0 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -59,8 +59,8 @@ async def test_registration(hass, hass_client, hass_admin_user): async def test_registration_encryption(hass, hass_client): """Test that registrations happen.""" try: - from nacl.secret import SecretBox from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox except (ImportError, OSError): pytest.skip("libnacl/libsodium is not installed") return diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index bedaa8ce739..dec919317f5 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -22,8 +22,8 @@ _LOGGER = logging.getLogger(__name__) def encrypt_payload(secret_key, payload): """Return a encrypted payload given a key and dictionary of data.""" try: - from nacl.secret import SecretBox from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox except (ImportError, OSError): pytest.skip("libnacl/libsodium is not installed") return @@ -45,8 +45,8 @@ def encrypt_payload(secret_key, payload): def decrypt_payload(secret_key, encrypted_data): """Return a decrypted payload given a key and a string of encrypted data.""" try: - from nacl.secret import SecretBox from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox except (ImportError, OSError): pytest.skip("libnacl/libsodium is not installed") return diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 9f47c63aa85..9f3694637f3 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1318,12 +1318,12 @@ def generate_ciphers(secret): # PyNaCl ciphertext generation will fail if the module # cannot be imported. However, the test for decryption # also relies on this library and won't be run without it. - import pickle import base64 + import pickle try: - from nacl.secret import SecretBox from nacl.encoding import Base64Encoder + from nacl.secret import SecretBox keylen = SecretBox.KEY_SIZE key = secret.encode("utf-8") @@ -1369,8 +1369,8 @@ def mock_cipher(): def mock_decrypt(ciphertext, key): """Decrypt/unpickle.""" - import pickle import base64 + import pickle (mkey, plaintext) = pickle.loads(base64.b64decode(ciphertext)) if key != mkey: diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index fdc60ca7262..2ae3724ee66 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -337,6 +337,7 @@ async def test_race_condition(hass, monkeypatch): async def test_not_connected(hass, monkeypatch): """Test Error when sending commands to a disconnected device.""" import pytest + from homeassistant.core import HomeAssistantError test_device = RflinkCommand("DUMMY_DEVICE") diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index 9e262fafa7e..aa187b80d8f 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -39,9 +39,10 @@ from .consts import ( from tests.common import async_fire_time_changed, async_mock_service if TYPE_CHECKING: - from tests.common import MockUser from aioswitcher.devices import SwitcherV2Device + from tests.common import MockUser + async def test_failed_config( hass: HomeAssistantType, mock_failed_bridge: Generator[None, Any, None] diff --git a/tests/hassfest/test_dependencies.py b/tests/hassfest/test_dependencies.py index b9690107619..dcef6d34844 100644 --- a/tests/hassfest/test_dependencies.py +++ b/tests/hassfest/test_dependencies.py @@ -2,6 +2,7 @@ import ast import pytest + from script.hassfest.dependencies import ImportCollector From b882304ec215ebddf69531b3840b92b3d8519c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 29 Aug 2020 11:07:48 +0300 Subject: [PATCH 428/862] Upgrade pydocstyle to 5.1.0 (#39374) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de54a5728d3..bc48c781690 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==5.0.2 + - pydocstyle==5.1.0 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 1594fa94990..48ee770b1d9 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -6,6 +6,6 @@ codespell==1.17.1 flake8-docstrings==1.5.0 flake8==3.8.3 isort==5.4.2 -pydocstyle==5.0.2 +pydocstyle==5.1.0 pyupgrade==2.7.2 yamllint==1.24.2 From 8302a7879e2d1ff1d072aed2458caf0aba8800e2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 29 Aug 2020 13:09:50 +0200 Subject: [PATCH 429/862] Catch bad devices when Google Sync (#39377) --- .../components/google_assistant/smart_home.py | 17 ++++-- .../google_assistant/test_smart_home.py | 59 +++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 71e0d252fe1..50ef200d171 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -83,15 +83,24 @@ async def async_devices_sync(hass, data, payload): ) agent_user_id = data.config.get_agent_user_id(data.context) - - devices = await asyncio.gather( + entities = async_get_entities(hass, data.config) + results = await asyncio.gather( *( entity.sync_serialize(agent_user_id) - for entity in async_get_entities(hass, data.config) + for entity in entities if entity.should_expose() - ) + ), + return_exceptions=True, ) + devices = [] + + for entity, result in zip(entities, results): + if isinstance(result, Exception): + _LOGGER.error("Error serializing %s", entity.entity_id, exc_info=result) + else: + devices.append(result) + response = {"agentUserId": agent_user_id, "devices": devices} await data.config.async_connect_agent_user(agent_user_id) diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index f3bd42c1181..521f52a99eb 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1129,3 +1129,62 @@ async def test_reachable_devices(hass): "requestId": REQ_ID, "payload": {"devices": [{"verificationId": "light.ceiling_lights"}]}, } + + +async def test_sync_message_recovery(hass, caplog): + """Test a sync message recovers from bad entities.""" + light = DemoLight( + None, + "Demo Light", + state=False, + hs_color=(180, 75), + ) + light.hass = hass + light.entity_id = "light.demo_light" + await light.async_update_ha_state() + + hass.states.async_set( + "light.bad_light", + "on", + { + "min_mireds": "badvalue", + "supported_features": hass.components.light.SUPPORT_COLOR_TEMP, + }, + ) + + result = await sh.async_handle_message( + hass, + BASIC_CONFIG, + "test-agent", + {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]}, + const.SOURCE_CLOUD, + ) + + assert result == { + "requestId": REQ_ID, + "payload": { + "agentUserId": "test-agent", + "devices": [ + { + "id": "light.demo_light", + "name": {"name": "Demo Light"}, + "attributes": { + "colorModel": "hsv", + "colorTemperatureRange": { + "temperatureMaxK": 6535, + "temperatureMinK": 2000, + }, + }, + "traits": [ + "action.devices.traits.Brightness", + "action.devices.traits.OnOff", + "action.devices.traits.ColorSetting", + ], + "willReportState": False, + "type": "action.devices.types.LIGHT", + }, + ], + }, + } + + assert "Error serializing light.bad_light" in caplog.text From a9b611d3ed8744c2af595459293438657cccd9db Mon Sep 17 00:00:00 2001 From: Berni Moses Date: Sat, 29 Aug 2020 16:22:37 +0200 Subject: [PATCH 430/862] Bump python-temescal to 0.3 for lg_soundbar (#39379) --- homeassistant/components/lg_soundbar/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index 8ec7c789166..d7bc310253d 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -2,6 +2,6 @@ "domain": "lg_soundbar", "name": "LG Soundbars", "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", - "requirements": ["temescal==0.2"], + "requirements": ["temescal==0.3"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 8a3221cd355..f53c7894395 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2106,7 +2106,7 @@ tellcore-py==1.1.2 tellduslive==0.10.11 # homeassistant.components.lg_soundbar -temescal==0.2 +temescal==0.3 # homeassistant.components.temper temperusb==1.5.3 From 22a123fd4b9d8dc41fe2de46bea3a02154ed9c58 Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Sun, 30 Aug 2020 00:34:25 +0800 Subject: [PATCH 431/862] Support acpartner in aqara discovery (#37926) --- homeassistant/components/xiaomi_aqara/config_flow.py | 5 ++++- homeassistant/components/xiaomi_aqara/const.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index c42598c2665..6bf1aa4f4ee 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -18,6 +18,7 @@ from .const import ( CONF_SID, DEFAULT_DISCOVERY_RETRY, DOMAIN, + ZEROCONF_ACPARTNER, ZEROCONF_GATEWAY, ) @@ -158,7 +159,9 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_xiaomi_aqara") # Check if the discovered device is an xiaomi aqara gateway. - if not name.startswith(ZEROCONF_GATEWAY): + if not ( + name.startswith(ZEROCONF_GATEWAY) or name.startswith(ZEROCONF_ACPARTNER) + ): _LOGGER.debug( "Xiaomi device '%s' discovered with host %s, not identified as xiaomi aqara gateway", name, diff --git a/homeassistant/components/xiaomi_aqara/const.py b/homeassistant/components/xiaomi_aqara/const.py index 0eb117cdf3c..fcfa3939c2c 100644 --- a/homeassistant/components/xiaomi_aqara/const.py +++ b/homeassistant/components/xiaomi_aqara/const.py @@ -6,6 +6,7 @@ GATEWAYS_KEY = "gateways" LISTENER_KEY = "listener" ZEROCONF_GATEWAY = "lumi-gateway" +ZEROCONF_ACPARTNER = "lumi-acpartner" CONF_INTERFACE = "interface" CONF_PROTOCOL = "protocol" From 54ef16f01a82377d28017ddef97c61823bff768a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Aug 2020 15:23:57 -0500 Subject: [PATCH 432/862] Reload notify platforms concurrently with asyncio.gather (#39384) --- homeassistant/components/notify/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index aa6de463e8c..77ec48c5435 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -66,8 +66,11 @@ async def async_reload(hass, integration_name): ): return - for data in hass.data[NOTIFY_SERVICES][integration_name]: - await _async_setup_notify_services(hass, data) + tasks = [ + _async_setup_notify_services(hass, data) + for data in hass.data[NOTIFY_SERVICES][integration_name] + ] + await asyncio.gather(*tasks) async def _async_setup_notify_services(hass, data): From 7469f57a7b35d18c290d53ef1cc557ad56338bbc Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 29 Aug 2020 16:47:00 -0500 Subject: [PATCH 433/862] Add config flow to nzbget (#38938) * work on config flow * Update test_init.py * work on config flow * Update test_config_flow.py * Update test_config_flow.py * Update __init__.py * Update test_config_flow.py * Update __init__.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update __init__.py * Update __init__.py * Update __init__.py * Apply suggestions from code review Co-authored-by: J. Nick Koston * Update __init__.py * Update __init__.py * Update __init__.py * Update config_flow.py * Update __init__.py * Update __init__.py * Create coordinator.py * Update __init__.py * Update sensor.py * Update __init__.py * Update .coveragerc * Update coordinator.py * Update __init__.py * Update coordinator.py * Update __init__.py * Update coordinator.py * Update config_flow.py * Update __init__.py * Update coordinator.py * Update __init__.py * Update test_config_flow.py * Update coordinator.py * Update test_config_flow.py * Update test_init.py * Update homeassistant/components/nzbget/coordinator.py * Update test_config_flow.py Co-authored-by: J. Nick Koston --- .coveragerc | 2 +- homeassistant/components/nzbget/__init__.py | 289 +++++++++--------- .../components/nzbget/config_flow.py | 143 +++++++++ homeassistant/components/nzbget/const.py | 22 ++ .../components/nzbget/coordinator.py | 94 ++++++ homeassistant/components/nzbget/manifest.json | 3 +- homeassistant/components/nzbget/sensor.py | 130 ++++---- homeassistant/components/nzbget/strings.json | 36 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/nzbget/__init__.py | 119 ++++++++ tests/components/nzbget/test_config_flow.py | 156 ++++++++++ tests/components/nzbget/test_init.py | 66 ++++ 13 files changed, 852 insertions(+), 212 deletions(-) create mode 100644 homeassistant/components/nzbget/config_flow.py create mode 100644 homeassistant/components/nzbget/const.py create mode 100644 homeassistant/components/nzbget/coordinator.py create mode 100644 homeassistant/components/nzbget/strings.json create mode 100644 tests/components/nzbget/__init__.py create mode 100644 tests/components/nzbget/test_config_flow.py create mode 100644 tests/components/nzbget/test_init.py diff --git a/.coveragerc b/.coveragerc index 663031c1589..dc68403fb99 100644 --- a/.coveragerc +++ b/.coveragerc @@ -589,7 +589,7 @@ omit = homeassistant/components/nuki/lock.py homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py - homeassistant/components/nzbget/__init__.py + homeassistant/components/nzbget/coordinator.py homeassistant/components/nzbget/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/* diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 40a30d31743..9976d2fffc8 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -1,10 +1,10 @@ -"""The nzbget component.""" -from datetime import timedelta +"""The NZBGet integration.""" +import asyncio import logging -import pynzbgetapi import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -14,31 +14,30 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + ATTR_SPEED, + DATA_COORDINATOR, + DATA_UNDO_UPDATE_LISTENER, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_SPEED_LIMIT, + DEFAULT_SSL, + DOMAIN, + SERVICE_PAUSE, + SERVICE_RESUME, + SERVICE_SET_SPEED, +) +from .coordinator import NZBGetDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -ATTR_SPEED = "speed" - -DOMAIN = "nzbget" -DATA_NZBGET = "data_nzbget" -DATA_UPDATED = "nzbget_data_updated" - -DEFAULT_NAME = "NZBGet" -DEFAULT_PORT = 6789 -DEFAULT_SPEED_LIMIT = 1000 # 1 Megabyte/Sec - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) - -SERVICE_PAUSE = "pause" -SERVICE_RESUME = "resume" -SERVICE_SET_SPEED = "set_speed" - -SPEED_LIMIT_SCHEMA = vol.Schema( - {vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int} -) +PLATFORMS = ["sensor"] CONFIG_SCHEMA = vol.Schema( { @@ -52,147 +51,155 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.time_period, - vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, } ) }, extra=vol.ALLOW_EXTRA, ) +SPEED_LIMIT_SCHEMA = vol.Schema( + {vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int} +) -def setup(hass, config): - """Set up the NZBGet sensors.""" - host = config[DOMAIN][CONF_HOST] - port = config[DOMAIN][CONF_PORT] - ssl = "s" if config[DOMAIN][CONF_SSL] else "" - name = config[DOMAIN][CONF_NAME] - username = config[DOMAIN].get(CONF_USERNAME) - password = config[DOMAIN].get(CONF_PASSWORD) - scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL] +async def async_setup(hass: HomeAssistantType, config: dict) -> bool: + """Set up the NZBGet integration.""" + hass.data.setdefault(DOMAIN, {}) - try: - nzbget_api = pynzbgetapi.NZBGetAPI(host, username, password, ssl, ssl, port) - nzbget_api.version() - except pynzbgetapi.NZBGetAPIException as conn_err: - _LOGGER.error("Error setting up NZBGet API: %s", conn_err) - return False + if hass.config_entries.async_entries(DOMAIN): + return True - _LOGGER.debug("Successfully validated NZBGet API connection") - - nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api) - nzbget_data.init_download_list() - nzbget_data.update() - - def service_handler(service): - """Handle service calls.""" - if service.service == SERVICE_PAUSE: - nzbget_data.pause_download() - elif service.service == SERVICE_RESUME: - nzbget_data.resume_download() - elif service.service == SERVICE_SET_SPEED: - limit = service.data[ATTR_SPEED] - nzbget_data.rate(limit) - - hass.services.register( - DOMAIN, SERVICE_PAUSE, service_handler, schema=vol.Schema({}) - ) - - hass.services.register( - DOMAIN, SERVICE_RESUME, service_handler, schema=vol.Schema({}) - ) - - hass.services.register( - DOMAIN, SERVICE_SET_SPEED, service_handler, schema=SPEED_LIMIT_SCHEMA - ) - - def refresh(event_time): - """Get the latest data from NZBGet.""" - nzbget_data.update() - - track_time_interval(hass, refresh, scan_interval) - - sensorconfig = {"client_name": name} - - hass.helpers.discovery.load_platform("sensor", DOMAIN, sensorconfig, config) + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], + ) + ) return True -class NZBGetData: - """Get the latest data and update the states.""" - - def __init__(self, hass, api): - """Initialize the NZBGet RPC API.""" - self.hass = hass - self.status = None - self.available = True - self._api = api - self.downloads = None - self.completed_downloads = set() - - def update(self): - """Get the latest data from NZBGet instance.""" - - try: - self.status = self._api.status() - self.downloads = self._api.history() - - self.check_completed_downloads() - - self.available = True - dispatcher_send(self.hass, DATA_UPDATED) - except pynzbgetapi.NZBGetAPIException as err: - self.available = False - _LOGGER.error("Unable to refresh NZBGet data: %s", err) - - def init_download_list(self): - """Initialize download list.""" - self.downloads = self._api.history() - self.completed_downloads = { - (x["Name"], x["Category"], x["Status"]) for x in self.downloads +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up NZBGet from a config entry.""" + if not entry.options: + options = { + CONF_SCAN_INTERVAL: entry.data.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), } + hass.config_entries.async_update_entry(entry, options=options) - def check_completed_downloads(self): - """Check history for newly completed downloads.""" + coordinator = NZBGetDataUpdateCoordinator( + hass, + config=entry.data, + options=entry.options, + ) - actual_completed_downloads = { - (x["Name"], x["Category"], x["Status"]) for x in self.downloads - } + await coordinator.async_refresh() - tmp_completed_downloads = list( - actual_completed_downloads.difference(self.completed_downloads) + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + undo_listener = entry.add_update_listener(_async_update_listener) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: coordinator, + DATA_UNDO_UPDATE_LISTENER: undo_listener, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) ) - for download in tmp_completed_downloads: - self.hass.bus.fire( - "nzbget_download_complete", - {"name": download[0], "category": download[1], "status": download[2]}, - ) + _async_register_services(hass, coordinator) - self.completed_downloads = actual_completed_downloads + return True - def pause_download(self): - """Pause download queue.""" - try: - self._api.pausedownload() - except pynzbgetapi.NZBGetAPIException as err: - _LOGGER.error("Unable to pause queue: %s", err) +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) - def resume_download(self): - """Resume download queue.""" + if unload_ok: + hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() + hass.data[DOMAIN].pop(entry.entry_id) - try: - self._api.resumedownload() - except pynzbgetapi.NZBGetAPIException as err: - _LOGGER.error("Unable to resume download queue: %s", err) + return unload_ok - def rate(self, limit): - """Set download speed.""" - try: - if not self._api.rate(limit): - _LOGGER.error("Limit was out of range") - except pynzbgetapi.NZBGetAPIException as err: - _LOGGER.error("Unable to set download speed: %s", err) +def _async_register_services( + hass: HomeAssistantType, + coordinator: NZBGetDataUpdateCoordinator, +) -> None: + """Register integration-level services.""" + + def pause(call) -> None: + """Service call to pause downloads in NZBGet.""" + coordinator.nzbget.pausedownload() + + def resume(call) -> None: + """Service call to resume downloads in NZBGet.""" + coordinator.nzbget.resumedownload() + + def set_speed(call) -> None: + """Service call to rate limit speeds in NZBGet.""" + coordinator.nzbget.rate(call.data[ATTR_SPEED]) + + hass.services.async_register(DOMAIN, SERVICE_PAUSE, pause, schema=vol.Schema({})) + hass.services.async_register(DOMAIN, SERVICE_RESUME, resume, schema=vol.Schema({})) + hass.services.async_register( + DOMAIN, SERVICE_SET_SPEED, set_speed, schema=SPEED_LIMIT_SCHEMA + ) + + +async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +class NZBGetEntity(Entity): + """Defines a base NZBGet entity.""" + + def __init__( + self, *, entry_id: str, name: str, coordinator: NZBGetDataUpdateCoordinator + ) -> None: + """Initialize the NZBGet entity.""" + self._name = name + self._entry_id = entry_id + self.coordinator = coordinator + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.last_update_success + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def should_poll(self) -> bool: + """Return the polling requirement of the entity.""" + return False + + async def async_added_to_hass(self) -> None: + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self) -> None: + """Request an update from the coordinator of this entity.""" + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py new file mode 100644 index 00000000000..77524cc3079 --- /dev/null +++ b/homeassistant/components/nzbget/config_flow.py @@ -0,0 +1,143 @@ +"""Config flow for NZBGet.""" +import logging +from typing import Any, Dict, Optional + +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow, OptionsFlow +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import callback +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import ( + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, +) +from .const import DOMAIN # pylint: disable=unused-import +from .coordinator import NZBGetAPI, NZBGetAPIException + +_LOGGER = logging.getLogger(__name__) + + +def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + nzbget_api = NZBGetAPI( + data[CONF_HOST], + data[CONF_USERNAME] if data[CONF_USERNAME] != "" else None, + data[CONF_PASSWORD] if data[CONF_PASSWORD] != "" else None, + data[CONF_SSL], + data[CONF_VERIFY_SSL], + data[CONF_PORT], + ) + + nzbget_api.version() + + return True + + +class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for NZBGet.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return NZBGetOptionsFlowHandler(config_entry) + + async def async_step_import( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by configuration file.""" + if CONF_SCAN_INTERVAL in user_input: + user_input[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL].seconds + + return await self.async_step_user(user_input) + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is None: + return self._show_setup_form() + + if CONF_VERIFY_SSL not in user_input: + user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL + + try: + await self.hass.async_add_executor_job( + validate_input, self.hass, user_input + ) + except NZBGetAPIException: + return self._show_setup_form({"base": "cannot_connect"}) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) + + def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the setup form to the user.""" + data_schema = { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, + } + + if self.show_advanced_options: + data_schema[ + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL) + ] = bool + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(data_schema), + errors=errors or {}, + ) + + +class NZBGetOptionsFlowHandler(OptionsFlow): + """Handle NZBGet client options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input: Optional[ConfigType] = None): + """Manage NZBGet options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): int, + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/nzbget/const.py b/homeassistant/components/nzbget/const.py new file mode 100644 index 00000000000..673f2531a53 --- /dev/null +++ b/homeassistant/components/nzbget/const.py @@ -0,0 +1,22 @@ +"""Constants for NZBGet.""" +DOMAIN = "nzbget" + +# Attributes +ATTR_SPEED = "speed" + +# Data +DATA_COORDINATOR = "corrdinator" +DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" + +# Defaults +DEFAULT_NAME = "NZBGet" +DEFAULT_PORT = 6789 +DEFAULT_SCAN_INTERVAL = 5 # time in seconds +DEFAULT_SPEED_LIMIT = 1000 # 1 Megabyte/Sec +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = False + +# Services +SERVICE_PAUSE = "pause" +SERVICE_RESUME = "resume" +SERVICE_SET_SPEED = "set_speed" diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py new file mode 100644 index 00000000000..8892475bc09 --- /dev/null +++ b/homeassistant/components/nzbget/coordinator.py @@ -0,0 +1,94 @@ +"""Provides the NZBGet DataUpdateCoordinator.""" +from datetime import timedelta +import logging + +from async_timeout import timeout +from pynzbgetapi import NZBGetAPI, NZBGetAPIException + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching NZBGet data.""" + + def __init__(self, hass: HomeAssistantType, *, config: dict, options: dict): + """Initialize global NZBGet data updater.""" + self.nzbget = NZBGetAPI( + config[CONF_HOST], + config[CONF_USERNAME] if config[CONF_USERNAME] != "" else None, + config[CONF_PASSWORD] if config[CONF_PASSWORD] != "" else None, + config[CONF_SSL], + config[CONF_VERIFY_SSL], + config[CONF_PORT], + ) + + self._completed_downloads_init = False + self._completed_downloads = {} + + update_interval = timedelta(seconds=options[CONF_SCAN_INTERVAL]) + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=update_interval, + ) + + def _check_completed_downloads(self, history): + """Check history for newly completed downloads.""" + actual_completed_downloads = { + (x["Name"], x["Category"], x["Status"]) for x in history + } + + if self._completed_downloads_init: + tmp_completed_downloads = list( + actual_completed_downloads.difference(self._completed_downloads) + ) + + for download in tmp_completed_downloads: + self.hass.bus.fire( + "nzbget_download_complete", + { + "name": download[0], + "category": download[1], + "status": download[2], + }, + ) + + self._completed_downloads = actual_completed_downloads + self._completed_downloads_init = True + + async def _async_update_data(self) -> dict: + """Fetch data from NZBGet.""" + + def _update_data() -> dict: + """Fetch data from NZBGet via sync functions.""" + status = self.nzbget.status() + history = self.nzbget.history() + + self._check_completed_downloads(history) + + return { + "status": status, + "downloads": history, + } + + try: + async with timeout(4): + return await self.hass.async_add_executor_job(_update_data) + except NZBGetAPIException as error: + raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 9aa84942cc5..7c5e9cf5e8d 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -3,5 +3,6 @@ "name": "NZBGet", "documentation": "https://www.home-assistant.io/integrations/nzbget", "requirements": ["pynzbgetapi==0.2.0"], - "codeowners": ["@chriscla"] + "codeowners": ["@chriscla"], + "config_flow": true } diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index a0f1dc57c94..e8e7b619f2c 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,21 +1,23 @@ """Monitor the NZBGet API.""" import logging +from typing import Callable, List, Optional +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_NAME, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND, TIME_MINUTES, ) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType -from . import DATA_NZBGET, DATA_UPDATED +from . import NZBGetEntity +from .const import DATA_COORDINATOR, DOMAIN +from .coordinator import NZBGetDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "NZBGet" - SENSOR_TYPES = { "article_cache": ["ArticleCacheMB", "Article Cache", DATA_MEGABYTES], "average_download_rate": [ @@ -34,90 +36,80 @@ SENSOR_TYPES = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Create NZBGet sensors.""" +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up NZBGet sensor based on a config entry.""" + coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] + sensors = [] - if discovery_info is None: - return - - nzbget_data = hass.data[DATA_NZBGET] - name = discovery_info["client_name"] - - devices = [] for sensor_config in SENSOR_TYPES.values(): - new_sensor = NZBGetSensor( - nzbget_data, sensor_config[0], name, sensor_config[1], sensor_config[2] + sensors.append( + NZBGetSensor( + coordinator, + entry.entry_id, + entry.data[CONF_NAME], + sensor_config[0], + sensor_config[1], + sensor_config[2], + ) ) - devices.append(new_sensor) - add_entities(devices, True) + async_add_entities(sensors, True) -class NZBGetSensor(Entity): +class NZBGetSensor(NZBGetEntity, Entity): """Representation of a NZBGet sensor.""" def __init__( - self, nzbget_data, sensor_type, client_name, sensor_name, unit_of_measurement + self, + coordinator: NZBGetDataUpdateCoordinator, + entry_id: str, + entry_name: str, + sensor_type: str, + sensor_name: str, + unit_of_measurement: Optional[str] = None, ): """Initialize a new NZBGet sensor.""" - self._name = f"{client_name} {sensor_name}" - self.type = sensor_type - self.client_name = client_name - self.nzbget_data = nzbget_data - self._state = None + self._sensor_type = sensor_type + self._unique_id = f"{entry_id}_{sensor_type}" self._unit_of_measurement = unit_of_measurement + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + name=f"{entry_name} {sensor_name}", + ) + @property - def name(self): - """Return the name of the sensor.""" - return self._name + def unique_id(self) -> str: + """Return the unique ID of the sensor.""" + return self._unique_id + + @property + def unit_of_measurement(self) -> str: + """Return the unit that the state of sensor is expressed in.""" + return self._unit_of_measurement @property def state(self): """Return the state of the sensor.""" - return self._state + value = self.coordinator.data.status.get(self._sensor_type) - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def available(self): - """Return whether the sensor is available.""" - return self.nzbget_data.available - - async def async_added_to_hass(self): - """Handle entity which will be added.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update - ) - ) - - @callback - def _schedule_immediate_update(self): - self.async_schedule_update_ha_state(True) - - def update(self): - """Update state of sensor.""" - - if self.nzbget_data.status is None: - _LOGGER.debug( - "Update of %s requested, but no status is available", self._name - ) - return - - value = self.nzbget_data.status.get(self.type) if value is None: - _LOGGER.warning("Unable to locate value for %s", self.type) - return + _LOGGER.warning("Unable to locate value for %s", self._sensor_type) + return None - if "DownloadRate" in self.type and value > 0: + if "DownloadRate" in self._sensor_type and value > 0: # Convert download rate from Bytes/s to MBytes/s - self._state = round(value / 2 ** 20, 2) - elif "UpTimeSec" in self.type and value > 0: + return round(value / 2 ** 20, 2) + + if "UpTimeSec" in self._sensor_type and value > 0: # Convert uptime from seconds to minutes - self._state = round(value / 60, 2) - else: - self._state = value + return round(value / 60, 2) + + return value diff --git a/homeassistant/components/nzbget/strings.json b/homeassistant/components/nzbget/strings.json new file mode 100644 index 00000000000..9bbcd66781e --- /dev/null +++ b/homeassistant/components/nzbget/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "title": "Connect to NZBGet", + "data": { + "name": "Name", + "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%]", + "ssl": "NZBGet uses a SSL certificate", + "verify_ssl": "NZBGet uses a proper certificate" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequency (seconds)" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ac9ebae5264..d8533554ea2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -124,6 +124,7 @@ FLOWS = [ "nuheat", "nut", "nws", + "nzbget", "onvif", "opentherm_gw", "openuv", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 511776e5f21..39eb8861c61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -736,6 +736,9 @@ pynws==1.2.1 # homeassistant.components.nx584 pynx584==0.5 +# homeassistant.components.nzbget +pynzbgetapi==0.2.0 + # homeassistant.components.openuv pyopenuv==1.0.9 diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py new file mode 100644 index 00000000000..8da67e2a0a2 --- /dev/null +++ b/tests/components/nzbget/__init__.py @@ -0,0 +1,119 @@ +"""Tests for the NZBGet integration.""" +from datetime import timedelta + +from homeassistant.components.nzbget.const import DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +ENTRY_CONFIG = { + CONF_HOST: "10.10.10.30", + CONF_NAME: "NZBGetTest", + CONF_PASSWORD: "", + CONF_PORT: 6789, + CONF_SSL: False, + CONF_USERNAME: "", + CONF_VERIFY_SSL: False, +} + +USER_INPUT = { + CONF_HOST: "10.10.10.30", + CONF_NAME: "NZBGet", + CONF_PASSWORD: "", + CONF_PORT: 6789, + CONF_SSL: False, + CONF_USERNAME: "", +} + +YAML_CONFIG = { + CONF_HOST: "10.10.10.30", + CONF_NAME: "GetNZBsTest", + CONF_PASSWORD: "", + CONF_PORT: 6789, + CONF_SCAN_INTERVAL: timedelta(seconds=5), + CONF_SSL: False, + CONF_USERNAME: "", +} + +MOCK_VERSION = "21.0" + +MOCK_STATUS = { + "ArticleCacheMB": "64", + "AverageDownloadRate": "512", + "DownloadPaused": "4", + "DownloadRate": "1000", + "DownloadedSizeMB": "256", + "FreeDiskSpaceMB": "1024", + "PostJobCount": "2", + "PostPaused": "4", + "RemainingSizeMB": "512", + "UpTimeSec": "600", +} + +MOCK_HISTORY = [ + {"Name": "Downloaded Item XYZ", "Category": "", "Status": "SUCCESS"}, + {"Name": "Failed Item ABC", "Category": "", "Status": "FAILURE"}, +] + + +async def init_integration( + hass, + *, + status: dict = MOCK_STATUS, + history: dict = MOCK_HISTORY, + version: str = MOCK_VERSION, +) -> MockConfigEntry: + """Set up the NZBGet integration in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) + entry.add_to_hass(hass) + + with _patch_version(version), _patch_status(status), _patch_history(history): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +def _patch_async_setup(return_value=True): + return patch( + "homeassistant.components.nzbget.async_setup", + return_value=return_value, + ) + + +def _patch_async_setup_entry(return_value=True): + return patch( + "homeassistant.components.nzbget.async_setup_entry", + return_value=return_value, + ) + + +def _patch_history(return_value=MOCK_HISTORY): + return patch( + "homeassistant.components.nzbget.coordinator.NZBGetAPI.history", + return_value=return_value, + ) + + +def _patch_status(return_value=MOCK_STATUS): + return patch( + "homeassistant.components.nzbget.coordinator.NZBGetAPI.status", + return_value=return_value, + ) + + +def _patch_version(return_value=MOCK_VERSION): + return patch( + "homeassistant.components.nzbget.coordinator.NZBGetAPI.version", + return_value=return_value, + ) diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py new file mode 100644 index 00000000000..362ba25ff67 --- /dev/null +++ b/tests/components/nzbget/test_config_flow.py @@ -0,0 +1,156 @@ +"""Test the NZBGet config flow.""" +from pynzbgetapi import NZBGetAPIException + +from homeassistant.components.nzbget.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_VERIFY_SSL +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.setup import async_setup_component + +from . import ( + ENTRY_CONFIG, + USER_INPUT, + _patch_async_setup, + _patch_async_setup_entry, + _patch_history, + _patch_status, + _patch_version, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_user_form(hass): + """Test we get the user initiated form.""" + await async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "10.10.10.30" + assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: False} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_form_show_advanced_options(hass): + """Test we get the user initiated form with advanced options shown.""" + await async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + user_input_advanced = { + **USER_INPUT, + CONF_VERIFY_SSL: True, + } + + with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input_advanced, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "10.10.10.30" + assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: True} + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch( + "homeassistant.components.nzbget.coordinator.NZBGetAPI.version", + side_effect=NZBGetAPIException(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_form_unexpected_exception(hass): + """Test we handle unexpected exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch( + "homeassistant.components.nzbget.coordinator.NZBGetAPI.version", + side_effect=Exception(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_user_form_single_instance_allowed(hass): + """Test that configuring more than one instance is rejected.""" + entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=USER_INPUT, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_options_flow(hass): + """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=ENTRY_CONFIG, + options={CONF_SCAN_INTERVAL: 5}, + ) + entry.add_to_hass(hass) + + assert entry.options[CONF_SCAN_INTERVAL] == 5 + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_SCAN_INTERVAL: 15}, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_SCAN_INTERVAL] == 15 diff --git a/tests/components/nzbget/test_init.py b/tests/components/nzbget/test_init.py new file mode 100644 index 00000000000..62532c56699 --- /dev/null +++ b/tests/components/nzbget/test_init.py @@ -0,0 +1,66 @@ +"""Test the NZBGet config flow.""" +from pynzbgetapi import NZBGetAPIException + +from homeassistant.components.nzbget.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.setup import async_setup_component + +from . import ( + ENTRY_CONFIG, + YAML_CONFIG, + _patch_async_setup_entry, + _patch_history, + _patch_status, + _patch_version, + init_integration, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_import_from_yaml(hass) -> None: + """Test import from YAML.""" + with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry(): + assert await async_setup_component(hass, DOMAIN, {DOMAIN: YAML_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + assert entries[0].data[CONF_NAME] == "GetNZBsTest" + assert entries[0].data[CONF_HOST] == "10.10.10.30" + assert entries[0].data[CONF_PORT] == 6789 + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_NOT_LOADED + assert not hass.data.get(DOMAIN) + + +async def test_async_setup_raises_entry_not_ready(hass): + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG) + config_entry.add_to_hass(hass) + + with _patch_version(), patch( + "homeassistant.components.nzbget.coordinator.NZBGetAPI.status", + side_effect=NZBGetAPIException(), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + + assert config_entry.state == ENTRY_STATE_SETUP_RETRY From 867d5088e3385b87e938940f1c2d52f16776569b Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Sat, 29 Aug 2020 18:03:19 -0400 Subject: [PATCH 434/862] Fix bond fan.turn_on with OFF speed (#39387) --- homeassistant/components/bond/fan.py | 12 +++++++++++- tests/components/bond/test_fan.py | 12 ++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index cb247b37309..14a5f84c8b1 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -1,4 +1,5 @@ """Support for Bond fans.""" +import logging import math from typing import Any, Callable, List, Optional @@ -23,6 +24,8 @@ from .const import DOMAIN from .entity import BondEntity from .utils import BondDevice, BondHub +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -97,6 +100,8 @@ class BondFan(BondEntity, FanEntity): async def async_set_speed(self, speed: str) -> None: """Set the desired speed for the fan.""" + _LOGGER.debug("async_set_speed called with speed %s", speed) + max_speed = self._device.props.get("max_speed", 3) if speed == SPEED_LOW: bond_speed = 1 @@ -111,8 +116,13 @@ class BondFan(BondEntity, FanEntity): async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None: """Turn on the fan.""" + _LOGGER.debug("async_turn_on called with speed %s", speed) + if speed is not None: - await self.async_set_speed(speed) + if speed == SPEED_OFF: + await self.async_turn_off() + else: + await self.async_set_speed(speed) else: await self._hub.bond.action(self._device.device_id, Action.turn_on()) diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 72770817110..0e0e980c39b 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -144,6 +144,18 @@ async def test_turn_on_fan_without_speed(hass: core.HomeAssistant): mock_turn_on.assert_called_with("test-device-id", Action.turn_on()) +async def test_turn_on_fan_with_off_speed(hass: core.HomeAssistant): + """Tests that turn on command delegates to turn off API.""" + await setup_platform( + hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" + ) + + with patch_bond_action() as mock_turn_off, patch_bond_device_state(): + await turn_fan_on(hass, "fan.name_1", fan.SPEED_OFF) + + mock_turn_off.assert_called_with("test-device-id", Action.turn_off()) + + async def test_turn_off_fan(hass: core.HomeAssistant): """Tests that turn off command delegates to API.""" await setup_platform( From 79252c06b2bbb1f7722f62a03c5d14edfc4d84c7 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 30 Aug 2020 00:43:09 +0200 Subject: [PATCH 435/862] Upgrade eternalegypt to 0.0.12 (#39386) --- homeassistant/components/netgear_lte/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 9f91a3a66c0..e910132e784 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -2,6 +2,6 @@ "domain": "netgear_lte", "name": "NETGEAR LTE", "documentation": "https://www.home-assistant.io/integrations/netgear_lte", - "requirements": ["eternalegypt==0.0.11"], + "requirements": ["eternalegypt==0.0.12"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index f53c7894395..1872b1e4d23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -567,7 +567,7 @@ epson-projector==0.1.3 epsonprinter==0.0.9 # homeassistant.components.netgear_lte -eternalegypt==0.0.11 +eternalegypt==0.0.12 # homeassistant.components.keyboard_remote # evdev==1.1.2 From 800cf6c8c0b1d688ab0a364d719bc908539ed3cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 30 Aug 2020 00:45:54 +0200 Subject: [PATCH 436/862] Revert "Support selecting http vs https protocols for qvrpro (#38951)" (#39385) This reverts commit 526c418e1e060120c16e75ab3d3d7c3cad7eb33f. --- homeassistant/components/qvr_pro/__init__.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/qvr_pro/__init__.py b/homeassistant/components/qvr_pro/__init__.py index d7f5c3e93cb..ed12cd49c51 100644 --- a/homeassistant/components/qvr_pro/__init__.py +++ b/homeassistant/components/qvr_pro/__init__.py @@ -8,13 +8,7 @@ from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_PROTOCOL, - CONF_USERNAME, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -26,7 +20,6 @@ from .const import ( ) DEFAULT_PORT = 8080 -DEFAULT_PROTOCOL = "http" SERVICE_CHANNEL_GUID = "guid" @@ -40,9 +33,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In(["http", "https"]) - ), vol.Optional(CONF_EXCLUDE_CHANNELS, default=[]): vol.All( cv.ensure_list_csv, [cv.positive_int] ), @@ -64,11 +54,10 @@ def setup(hass, config): password = conf[CONF_PASSWORD] host = conf[CONF_HOST] port = conf[CONF_PORT] - protocol = conf[CONF_PROTOCOL] excluded_channels = conf[CONF_EXCLUDE_CHANNELS] try: - qvrpro = Client(user, password, host, protocol=protocol, port=port) + qvrpro = Client(user, password, host, port=port) channel_resp = qvrpro.get_channel_list() From 904f5c4346bc98d15270152d4291d325160a9380 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 30 Aug 2020 00:03:17 +0000 Subject: [PATCH 437/862] [ci skip] Translation update --- .../accuweather/translations/cs.json | 12 ++++++ .../binary_sensor/translations/cs.json | 40 ++++++++++++++++--- .../components/demo/translations/cs.json | 11 +++++ .../components/insteon/translations/cs.json | 11 +++++ .../components/kodi/translations/cs.json | 16 ++++++++ .../components/local_ip/translations/cs.json | 11 +++++ .../components/nut/translations/cs.json | 7 ++++ .../components/nzbget/translations/en.json | 36 +++++++++++++++++ .../components/onvif/translations/cs.json | 1 + .../components/risco/translations/no.json | 4 +- .../season/translations/sensor.cs.json | 6 +++ .../components/sensor/translations/cs.json | 6 ++- .../components/sentry/translations/cs.json | 11 +++++ .../components/shelly/translations/cs.json | 18 +++++++++ .../components/tag/translations/cs.json | 3 ++ .../components/unifi/translations/cs.json | 31 +++++++++++--- .../components/wilight/translations/cs.json | 15 +++++++ 17 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/cs.json create mode 100644 homeassistant/components/demo/translations/cs.json create mode 100644 homeassistant/components/insteon/translations/cs.json create mode 100644 homeassistant/components/kodi/translations/cs.json create mode 100644 homeassistant/components/local_ip/translations/cs.json create mode 100644 homeassistant/components/nzbget/translations/en.json create mode 100644 homeassistant/components/sentry/translations/cs.json create mode 100644 homeassistant/components/shelly/translations/cs.json create mode 100644 homeassistant/components/tag/translations/cs.json create mode 100644 homeassistant/components/wilight/translations/cs.json diff --git a/homeassistant/components/accuweather/translations/cs.json b/homeassistant/components/accuweather/translations/cs.json new file mode 100644 index 00000000000..80383c42462 --- /dev/null +++ b/homeassistant/components/accuweather/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + } + } + } + } +} \ 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 a656a7274ba..ae0745d3a49 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -1,8 +1,29 @@ { "device_automation": { "condition_type": { + "is_bat_low": "Baterie {entity_name} je skoro vybit\u00e1", + "is_cold": "{entity_name} je studen\u00fd", + "is_connected": "{entity_name} je p\u0159ipojen", + "is_gas": "{entity_name} detekuje plyn", + "is_hot": "{entity_name} je hork\u00fd", + "is_light": "{entity_name} detekuje sv\u011btlo", "is_locked": "{entity_name} je zam\u010deno", + "is_moist": "{entity_name} je vlhk\u00fd", + "is_motion": "{entity_name} detekuje pohyb", + "is_moving": "{entity_name} se pohybuje", + "is_no_gas": "{entity_name} nedetekuje plyn", + "is_no_light": "{entity_name} nedetekuje sv\u011btlo", + "is_no_motion": "{entity_name} nedetekuje pohyb", + "is_no_problem": "{entity_name} nehl\u00e1s\u00ed probl\u00e9m", + "is_no_smoke": "{entity_name} nedetekuje kou\u0159", + "is_no_sound": "{entity_name} nedetekuje zvuk", + "is_no_vibration": "{entity_name} nedetekuje vibrace", "is_not_bat_low": "{entity_name} baterie v norm\u00e1lu", + "is_not_cold": "{entity_name} nen\u00ed studen\u00fd", + "is_not_connected": "{entity_name} je odpojen", + "is_not_hot": "{entity_name} nen\u00ed hork\u00fd", + "is_not_locked": "{entity_name} je odem\u010den", + "is_not_moist": "{entity_name} je such\u00fd", "is_not_moving": "{entity_name} se nepohybuje", "is_not_occupied": "{entity_name} nen\u00ed obsazeno", "is_not_open": "{entity_name} je zav\u0159eno", @@ -27,8 +48,13 @@ "bat_low": "{entity_name} vybit\u00e1 baterie", "cold": "{entity_name} vychladlo", "connected": "{entity_name} p\u0159ipojeno", + "gas": "{entity_name} za\u010dalo detekovat plyn", + "hot": "{entity_name} se zah\u0159\u00e1l", + "light": "{entity_name} za\u010dalo detekovat sv\u011btlo", "locked": "{entity_name} zam\u010deno", "moist": "{entity_name} se navlh\u010dil", + "motion": "{entity_name} za\u010dalo detekovat pohyb", + "moving": "{entity_name} se za\u010dal pohybovat", "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", @@ -37,7 +63,9 @@ "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_cold": "{entity_name} p\u0159estal b\u00fdt studen\u00fd", "not_connected": "{entity_name} odpojeno", + "not_hot": "{entity_name} p\u0159estal b\u00fdt hork\u00fd", "not_locked": "{entity_name} odem\u010deno", "not_moist": "{entity_name} vyschlo", "not_moving": "{entity_name} se p\u0159estalo pohybovat", @@ -52,13 +80,13 @@ "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", + "problem": "{entity_name} za\u010dalo detekovat probl\u00e9m", + "smoke": "{entity_name} za\u010dalo detekovat kou\u0159", + "sound": "{entity_name} za\u010dalo detekovat 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" + "vibration": "{entity_name} za\u010dalo detekovat vibrace" } }, "state": { @@ -72,7 +100,7 @@ }, "cold": { "off": "Norm\u00e1ln\u00ed", - "on": "Chladn\u00e9" + "on": "Studen\u00e9" }, "connectivity": { "off": "Odpojeno", @@ -103,7 +131,7 @@ "on": "Vlhko" }, "motion": { - "off": "Bez pohybu", + "off": "\u017d\u00e1dn\u00fd pohyb", "on": "Zaznamen\u00e1n pohyb" }, "occupancy": { diff --git a/homeassistant/components/demo/translations/cs.json b/homeassistant/components/demo/translations/cs.json new file mode 100644 index 00000000000..5fe8151103e --- /dev/null +++ b/homeassistant/components/demo/translations/cs.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "options_1": { + "data": { + "int": "\u010c\u00edseln\u00fd vstup" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/cs.json b/homeassistant/components/insteon/translations/cs.json new file mode 100644 index 00000000000..354a7eb70b8 --- /dev/null +++ b/homeassistant/components/insteon/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "hub2": { + "data": { + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/cs.json b/homeassistant/components/kodi/translations/cs.json new file mode 100644 index 00000000000..3857095fce2 --- /dev/null +++ b/homeassistant/components/kodi/translations/cs.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "host": { + "data": { + "ssl": "P\u0159ipojen\u00ed p\u0159es SSL" + } + }, + "user": { + "data": { + "ssl": "P\u0159ipojen\u00ed p\u0159es SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/cs.json b/homeassistant/components/local_ip/translations/cs.json new file mode 100644 index 00000000000..8130726dad9 --- /dev/null +++ b/homeassistant/components/local_ip/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "N\u00e1zev senzoru" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/cs.json b/homeassistant/components/nut/translations/cs.json index 23ddc89a94d..5940a9c60ad 100644 --- a/homeassistant/components/nut/translations/cs.json +++ b/homeassistant/components/nut/translations/cs.json @@ -10,5 +10,12 @@ } } } + }, + "options": { + "step": { + "init": { + "description": "Zvolte zdroje senzoru." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/en.json b/homeassistant/components/nzbget/translations/en.json new file mode 100644 index 00000000000..c410d4e4874 --- /dev/null +++ b/homeassistant/components/nzbget/translations/en.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "unknown": "Unexpected error" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "ssl": "NZBGet uses a SSL certificate", + "username": "Username", + "verify_ssl": "NZBGet uses a proper certificate" + }, + "title": "Connect to NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequency (seconds)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/cs.json b/homeassistant/components/onvif/translations/cs.json index dad373fb5e3..66d5e1f0093 100644 --- a/homeassistant/components/onvif/translations/cs.json +++ b/homeassistant/components/onvif/translations/cs.json @@ -33,6 +33,7 @@ "manual_input": { "data": { "host": "Adresa za\u0159\u00edzen\u00ed", + "name": "N\u00e1zev", "port": "Port" }, "title": "Konfigurovat za\u0159\u00edzen\u00ed ONVIF" diff --git a/homeassistant/components/risco/translations/no.json b/homeassistant/components/risco/translations/no.json index 48f966bd37c..bf438991b02 100644 --- a/homeassistant/components/risco/translations/no.json +++ b/homeassistant/components/risco/translations/no.json @@ -28,7 +28,7 @@ "armed_night": "Sikret Natt" }, "description": "Velg hvilken tilstand du vil stille Risco-alarmen til n\u00e5r du aktiverer Home Assistant-alarmen", - "title": "Kart Hjem Assistant oppgir til Risco stater" + "title": "Koble Home Assistant statuser til Risco statuser" }, "init": { "data": { @@ -48,7 +48,7 @@ "partial_arm": "Delvis Sikret (OPPHOLD)" }, "description": "Velg hvilken tilstand Home Assistant-alarmen skal rapportere for hver delstat som er rapportert av Risco", - "title": "Kart Risco oppgir til Home Assistant stater" + "title": "Koble Risco statuser til Home Assistant statuser" } } } diff --git a/homeassistant/components/season/translations/sensor.cs.json b/homeassistant/components/season/translations/sensor.cs.json index a13e4c1b3c3..31a4b2ef5a9 100644 --- a/homeassistant/components/season/translations/sensor.cs.json +++ b/homeassistant/components/season/translations/sensor.cs.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "Podzim", + "spring": "Jaro", + "summer": "L\u00e9to", + "winter": "Zima" + }, "season__season__": { "autumn": "Podzim", "spring": "Jaro", diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index 53a2da0872b..add81cbe07e 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -9,7 +9,8 @@ "is_signal_strength": "Aktu\u00e1ln\u00ed s\u00edla sign\u00e1lu {entity_name}", "is_temperature": "Aktu\u00e1ln\u00ed teplota {entity_name}", "is_timestamp": "Aktu\u00e1ln\u00ed \u010dasov\u00e9 raz\u00edtko {entity_name}", - "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}" + "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}", + "is_voltage": "Aktu\u00e1ln\u00ed nap\u011bt\u00ed {entity_name}" }, "trigger_type": { "battery_level": "\u00farove\u0148 baterie {entity_name} se zm\u011bn\u00ed", @@ -20,7 +21,8 @@ "signal_strength": "s\u00edla sign\u00e1lu {entity_name} se zm\u011bn\u00ed", "temperature": "teplota {entity_name} se zm\u011bn\u00ed", "timestamp": "\u010dasov\u00e9 raz\u00edtko {entity_name} se zm\u011bn\u00ed", - "value": "hodnota {entity_name} se zm\u011bn\u00ed" + "value": "hodnota {entity_name} se zm\u011bn\u00ed", + "voltage": "P\u0159i zm\u011bn\u011b nap\u011bt\u00ed {entity_name}" } }, "state": { diff --git a/homeassistant/components/sentry/translations/cs.json b/homeassistant/components/sentry/translations/cs.json new file mode 100644 index 00000000000..81105c211fd --- /dev/null +++ b/homeassistant/components/sentry/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "dsn": "DSN" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/cs.json b/homeassistant/components/shelly/translations/cs.json new file mode 100644 index 00000000000..13ad3d8edbd --- /dev/null +++ b/homeassistant/components/shelly/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "auth_not_supported": "Za\u0159\u00edzen\u00ed Shelly vy\u017eaduj\u00edc\u00ed autentizaci nejsou aktu\u00e1ln\u011b podporov\u00e1na.", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "Chcete nastavit {model} na adrese {host}?" + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/cs.json b/homeassistant/components/tag/translations/cs.json new file mode 100644 index 00000000000..55ef9373f14 --- /dev/null +++ b/homeassistant/components/tag/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "Zna\u010dka" +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json index fdbfbc343de..a2adfcce308 100644 --- a/homeassistant/components/unifi/translations/cs.json +++ b/homeassistant/components/unifi/translations/cs.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "already_configured": "\u0158adi\u010d je ji\u017e nakonfigurov\u00e1n" + "already_configured": "Ovlada\u010d je ji\u017e nakonfigurov\u00e1n" }, "error": { "faulty_credentials": "Chybn\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje", - "service_unavailable": "Slu\u017eba nen\u00ed dostupn\u00e1" + "service_unavailable": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown_client_mac": "Na t\u00e9to MAC adrese nen\u00ed dostupn\u00fd \u017e\u00e1dn\u00fd klient" }, "step": { "user": { @@ -15,14 +16,30 @@ "port": "Port", "site": "ID s\u00edt\u011b", "username": "U\u017eivatelsk\u00e9 jm\u00e9no", - "verify_ssl": "\u0158adi\u010d pou\u017e\u00edv\u00e1 spr\u00e1vn\u00fd certifik\u00e1t" + "verify_ssl": "Ovlada\u010d pou\u017e\u00edv\u00e1 spr\u00e1vn\u00fd certifik\u00e1t" }, - "title": "Nastaven\u00ed UniFi \u0159adi\u010de" + "title": "Nastaven\u00ed UniFi ovlada\u010de" } } }, "options": { "step": { + "client_control": { + "data": { + "poe_clients": "Povolit u klient\u016f ovl\u00e1d\u00e1n\u00ed POE" + }, + "title": "Mo\u017enosti UniFi 2/3" + }, + "device_tracker": { + "data": { + "ssid_filter": "Vyberte SSID, ve kter\u00e9m budou sledov\u00e1ni bezdr\u00e1tov\u011b p\u0159ipojen\u00ed klienti", + "track_clients": "Sledovat p\u0159ipojen\u00e9 klienty", + "track_devices": "Sledovat p\u0159ipojen\u00e1 za\u0159\u00edzen\u00ed (Ubiquiti za\u0159\u00edzen\u00ed)", + "track_wired_clients": "V\u010detn\u011b klient\u016f p\u0159ipojen\u00fdch kabelem" + }, + "description": "Konfigurace sledov\u00e1n\u00ed za\u0159\u00edzen\u00ed", + "title": "Mo\u017enosti UniFi 1/3" + }, "simple_options": { "data": { "track_clients": "Sledov\u00e1n\u00ed p\u0159ipojen\u00fdch za\u0159\u00edzen\u00ed", @@ -32,8 +49,10 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro s\u00ed\u0165ov\u00e9 klienty" - } + "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro p\u0159ipojen\u00e9 klienty" + }, + "description": "Konfigurovat statistick\u00e9 senzory", + "title": "Mo\u017enosti UniFi 3/3" } } } diff --git a/homeassistant/components/wilight/translations/cs.json b/homeassistant/components/wilight/translations/cs.json new file mode 100644 index 00000000000..0dcaaf15e5d --- /dev/null +++ b/homeassistant/components/wilight/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_supported_device": "Toto za\u0159\u00edzen\u00ed WiLight nen\u00ed moment\u00e1ln\u011b podporov\u00e1no.", + "not_wilight_device": "Toto za\u0159\u00edzen\u00ed nen\u00ed WiLight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "Chcete nastavit WiLight {name} ? \n\n Podporuje: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file From ad0d3b48480d6d2dcaa77e0d7f8ce8298b4621d1 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 29 Aug 2020 19:56:25 -0700 Subject: [PATCH 438/862] Improve handling of exceptions in Android TV (#39229) * Close the ADB connection when there is an exception * Add a test * Split a comment onto two lines * Fix test ('async_update' -> 'async_update_entity') * 'close' -> 'Close' --- .../components/androidtv/media_player.py | 8 ++- tests/components/androidtv/patchers.py | 8 ++- .../components/androidtv/test_media_player.py | 52 ++++++++++++++++--- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index a46685a1c65..959c85abd77 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -376,6 +376,12 @@ def adb_decorator(override_available=False): await self.aftv.adb_close() self._available = False return None + except Exception: + # An unforeseen exception occurred. Close the ADB connection so that + # it doesn't happen over and over again, then raise the exception. + await self.aftv.adb_close() + self._available = False # pylint: disable=protected-access + raise return _adb_exception_catcher @@ -421,10 +427,8 @@ class ADBDevice(MediaPlayerEntity): # Using "adb_shell" (Python ADB implementation) self.exceptions = ( AdbTimeoutError, - AttributeError, BrokenPipeError, ConnectionResetError, - TypeError, ValueError, InvalidChecksumError, InvalidCommandError, diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 98c38719cf0..a89abe33de3 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -111,7 +111,7 @@ def patch_shell(response=None, error=False): async def shell_fail_python(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpAsyncFake.shell` method when it fails.""" self.shell_cmd = cmd - raise AttributeError + raise ValueError async def shell_fail_server(self, cmd): """Mock the `DeviceAsyncFake.shell` method when it fails.""" @@ -182,3 +182,9 @@ def patch_androidtv_update( PATCH_LAUNCH_APP = patch("androidtv.basetv.basetv_async.BaseTVAsync.launch_app") PATCH_STOP_APP = patch("androidtv.basetv.basetv_async.BaseTVAsync.stop_app") + +# Cause the update to raise an unexpected type of exception +PATCH_ANDROIDTV_UPDATE_EXCEPTION = patch( + "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", + side_effect=ZeroDivisionError, +) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 2641c2b349d..456a9313091 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,8 +1,10 @@ """The tests for the androidtv platform.""" import base64 +import copy import logging from androidtv.exceptions import LockNotAcquiredException +import pytest from homeassistant.components.androidtv.media_player import ( ANDROIDTV_DOMAIN, @@ -294,7 +296,7 @@ async def test_adb_shell_returns_none_firetv_adb_server(hass): async def test_setup_with_adbkey(hass): """Test that setup succeeds when using an ADB key.""" - config = CONFIG_ANDROIDTV_PYTHON_ADB.copy() + config = copy.deepcopy(CONFIG_ANDROIDTV_PYTHON_ADB) config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey") patch_key, entity_id = _setup(config) @@ -313,7 +315,7 @@ async def test_setup_with_adbkey(hass): async def _test_sources(hass, config0): """Test that sources (i.e., apps) are handled correctly for Android TV and Fire TV devices.""" - config = config0.copy() + config = copy.deepcopy(config0) config[DOMAIN][CONF_APPS] = { "com.app.test1": "TEST 1", "com.app.test3": None, @@ -394,7 +396,7 @@ async def test_firetv_sources(hass): async def _test_exclude_sources(hass, config0, expected_sources): """Test that sources (i.e., apps) are handled correctly when the `exclude_unnamed_apps` config parameter is provided.""" - config = config0.copy() + config = copy.deepcopy(config0) config[DOMAIN][CONF_APPS] = { "com.app.test1": "TEST 1", "com.app.test3": None, @@ -453,21 +455,21 @@ async def _test_exclude_sources(hass, config0, expected_sources): async def test_androidtv_exclude_sources(hass): """Test that sources (i.e., apps) are handled correctly for Android TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" - config = CONFIG_ANDROIDTV_ADB_SERVER.copy() + config = copy.deepcopy(CONFIG_ANDROIDTV_ADB_SERVER) config[DOMAIN][CONF_EXCLUDE_UNNAMED_APPS] = True assert await _test_exclude_sources(hass, config, ["TEST 1"]) async def test_firetv_exclude_sources(hass): """Test that sources (i.e., apps) are handled correctly for Fire TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" - config = CONFIG_FIRETV_ADB_SERVER.copy() + config = copy.deepcopy(CONFIG_FIRETV_ADB_SERVER) config[DOMAIN][CONF_EXCLUDE_UNNAMED_APPS] = True assert await _test_exclude_sources(hass, config, ["TEST 1"]) async def _test_select_source(hass, config0, source, expected_arg, method_patch): """Test that the methods for launching and stopping apps are called correctly when selecting a source.""" - config = config0.copy() + config = copy.deepcopy(config0) config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1", "com.app.test3": None} patch_key, entity_id = _setup(config) @@ -702,7 +704,7 @@ async def test_setup_two_devices(hass): config = { DOMAIN: [ CONFIG_ANDROIDTV_ADB_SERVER[DOMAIN], - CONFIG_FIRETV_ADB_SERVER[DOMAIN].copy(), + copy.deepcopy(CONFIG_FIRETV_ADB_SERVER[DOMAIN]), ] } config[DOMAIN][1][CONF_HOST] = "127.0.0.2" @@ -1145,7 +1147,7 @@ async def test_services_androidtv(hass): async def test_services_firetv(hass): """Test media player services for a Fire TV device.""" patch_key, entity_id = _setup(CONFIG_FIRETV_ADB_SERVER) - config = CONFIG_FIRETV_ADB_SERVER.copy() + config = copy.deepcopy(CONFIG_FIRETV_ADB_SERVER) config[DOMAIN][CONF_TURN_OFF_COMMAND] = "test off" config[DOMAIN][CONF_TURN_ON_COMMAND] = "test on" @@ -1177,3 +1179,37 @@ async def test_connection_closed_on_ha_stop(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert adb_close.called + + +async def test_exception(hass): + """Test that the ADB connection gets closed when there is an unforeseen exception. + + HA will attempt to reconnect on the next update. + """ + patch_key, entity_id = _setup(CONFIG_ANDROIDTV_PYTHON_ADB) + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ + patch_key + ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_PYTHON_ADB) + await hass.async_block_till_done() + + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + # When an unforessen exception occurs, we close the ADB connection and raise the exception + with patchers.PATCH_ANDROIDTV_UPDATE_EXCEPTION, pytest.raises(Exception): + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # On the next update, HA will reconnect to the device + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF From dda4cf4d580ca5b41f1d4b028494b1edccda6b84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 02:20:31 -0500 Subject: [PATCH 439/862] Tune logbook performance to accomodate recent changes (#39348) --- .../components/automation/logbook.py | 11 +++--- homeassistant/components/logbook/__init__.py | 39 ++++++++++++++----- homeassistant/components/script/logbook.py | 5 ++- tests/components/logbook/test_init.py | 10 +++++ 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index cc44296eaa6..3c9671af18f 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -12,14 +12,15 @@ def async_describe_events(hass, async_describe_event): # type: ignore @callback def async_describe_logbook_event(event): # type: ignore """Describe a logbook event.""" + data = event.data message = "has been triggered" - if ATTR_SOURCE in event.data: - message = f"{message} by {event.data[ATTR_SOURCE]}" + if ATTR_SOURCE in data: + message = f"{message} by {data[ATTR_SOURCE]}" return { - "name": event.data.get(ATTR_NAME), + "name": data.get(ATTR_NAME), "message": message, - "source": event.data.get(ATTR_SOURCE), - "entity_id": event.data.get(ATTR_ENTITY_ID), + "source": data.get(ATTR_SOURCE), + "entity_id": data.get(ATTR_ENTITY_ID), } async_describe_event( diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index fb219de0d8c..03dc1ffdef9 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -3,6 +3,7 @@ from datetime import timedelta from itertools import groupby import json import logging +import re import sqlalchemy from sqlalchemy.orm import aliased @@ -50,6 +51,9 @@ from homeassistant.helpers.integration_platform import ( from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util +ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": "([^"]+)"') +DOMAIN_JSON_EXTRACT = re.compile('"domain": "([^"]+)"') + _LOGGER = logging.getLogger(__name__) ATTR_MESSAGE = "message" @@ -485,20 +489,17 @@ def _keep_event(hass, event, entities_filter): entity_id = event.entity_id 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: - # If the entity_id isn't described, use the domain that describes - # the event for filtering. - domain = hass.data[DOMAIN][event.event_type][0] - if domain is None: - return False - entity_id = f"{domain}." elif event.event_type == EVENT_CALL_SERVICE: return False else: - event_data = event.data - entity_id = event_data.get(ATTR_ENTITY_ID) + entity_id = event.data_entity_id if not entity_id: - domain = event_data.get(ATTR_DOMAIN) + if event.event_type in hass.data[DOMAIN]: + # If the entity_id isn't described, use the domain that describes + # the event for filtering. + domain = hass.data[DOMAIN][event.event_type][0] + else: + domain = event.data_domain if domain is None: return False entity_id = f"{domain}." @@ -689,6 +690,24 @@ class LazyEventPartialState: self.context_user_id = self._row.context_user_id self.time_fired_minute = self._row.time_fired.minute + @property + def data_entity_id(self): + """Extract the entity id from the decoded data or json.""" + if self._event_data: + return self._event_data.get(ATTR_ENTITY_ID) + + result = ENTITY_ID_JSON_EXTRACT.search(self._row.event_data) + return result and result.group(1) + + @property + def data_domain(self): + """Extract the domain from the decoded data or json.""" + if self._event_data: + return self._event_data.get(ATTR_DOMAIN) + + result = DOMAIN_JSON_EXTRACT.search(self._row.event_data) + return result and result.group(1) + @property def attributes(self): """State attributes.""" diff --git a/homeassistant/components/script/logbook.py b/homeassistant/components/script/logbook.py index 72ff0d15fc7..f75584540d3 100644 --- a/homeassistant/components/script/logbook.py +++ b/homeassistant/components/script/logbook.py @@ -12,10 +12,11 @@ def async_describe_events(hass, async_describe_event): @callback def async_describe_logbook_event(event): """Describe the logbook event.""" + data = event.data return { - "name": event.data.get(ATTR_NAME), + "name": data.get(ATTR_NAME), "message": "started", - "entity_id": event.data.get(ATTR_ENTITY_ID), + "entity_id": data.get(ATTR_ENTITY_ID), } async_describe_event(DOMAIN, EVENT_SCRIPT_STARTED, async_describe_logbook_event) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 78753e2a67e..4edf630322a 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2084,6 +2084,16 @@ async def test_logbook_context_from_template(hass, hass_client): class MockLazyEventPartialState(ha.Event): """Minimal mock of a Lazy event.""" + @property + def data_entity_id(self): + """Lookup entity id.""" + return self.data.get(ATTR_ENTITY_ID) + + @property + def data_domain(self): + """Lookup domain.""" + return self.data.get(ATTR_DOMAIN) + @property def time_fired_minute(self): """Minute the event was fired.""" From 87425d4ab1b7be00c9405bc63fe912230d838f7c Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Sun, 30 Aug 2020 11:08:57 +0200 Subject: [PATCH 440/862] Add device_class safety to synology_dsm storage binary_sensors (#39310) * add device_class: safety to storage binary_sensors * Update binary_sensor.py * Update homeassistant/components/synology_dsm/binary_sensor.py Co-authored-by: Martin Hjelmare * Update binary_sensor.py * Import device_class Safety from homeassistant.components.binary_sensor * Update binary_sensor.py Co-authored-by: Martin Hjelmare --- .../components/synology_dsm/binary_sensor.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index a75f57db678..c95b4298f5d 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Synology DSM binary sensors.""" from typing import Dict -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SAFETY, + BinarySensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DISKS from homeassistant.helpers.typing import HomeAssistantType @@ -14,6 +17,8 @@ from .const import ( SYNO_API, ) +DEFAULT_DEVICE_CLASS = DEVICE_CLASS_SAFETY + async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -71,3 +76,8 @@ class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity): if attr is None: return None return attr + + @property + def device_class(self): + """Return the device class of this binary sensor.""" + return DEFAULT_DEVICE_CLASS From f8712b0e00fc3d27edab928b3a38190389461338 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 05:02:37 -0500 Subject: [PATCH 441/862] Create a CoordinatorEntity class to avoid repating code in integrations (#39388) --- homeassistant/helpers/update_coordinator.py | 33 ++++++++++++++++++++- tests/helpers/test_update_coordinator.py | 22 +++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 7b7e6af4d62..987f1d63eee 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -10,7 +10,7 @@ import aiohttp import requests from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers import event +from homeassistant.helpers import entity, event from homeassistant.util.dt import utcnow from .debounce import Debouncer @@ -190,3 +190,34 @@ class DataUpdateCoordinator(Generic[T]): for update_callback in self._listeners: update_callback() + + +class CoordinatorEntity(entity.Entity): + """A class for entities using DataUpdateCoordinator.""" + + def __init__(self, coordinator: DataUpdateCoordinator) -> None: + """Create the entity with a DataUpdateCoordinator.""" + self.coordinator = coordinator + + @property + def should_poll(self) -> bool: + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 56c53f1994c..73360a3053b 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -11,7 +11,7 @@ import requests from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock +from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed LOGGER = logging.getLogger(__name__) @@ -224,3 +224,23 @@ async def test_refresh_recover(crd, caplog): assert crd.last_update_success is True assert "Fetching test data recovered" in caplog.text + + +async def test_coordinator_entity(crd): + """Test the CoordinatorEntity class.""" + entity = update_coordinator.CoordinatorEntity(crd) + + assert entity.should_poll is False + + crd.last_update_success = False + assert entity.available is False + + await entity.async_update() + assert entity.available is True + + with patch( + "homeassistant.helpers.entity.Entity.async_on_remove" + ) as mock_async_on_remove: + await entity.async_added_to_hass() + + assert mock_async_on_remove.called From ef13da5555520b6f88b71a72bcae3bfb0535bfbf Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Aug 2020 06:19:51 -0500 Subject: [PATCH 442/862] Fix marytts sync requests within event loop (#39399) --- homeassistant/components/marytts/tts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index da8208e1883..f04f98896a5 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_get_engine(hass, config, discovery_info=None): +def get_engine(hass, config, discovery_info=None): """Set up MaryTTS speech component.""" return MaryTTSProvider(hass, config) @@ -80,7 +80,7 @@ class MaryTTSProvider(Provider): """Return a list of supported options.""" return SUPPORT_OPTIONS - async def async_get_tts_audio(self, message, language, options=None): + def get_tts_audio(self, message, language, options=None): """Load TTS from MaryTTS.""" effects = options[CONF_EFFECT] From 4aa8b6cad8c1fa893c476819aad846b7557cb400 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 30 Aug 2020 14:16:41 +0200 Subject: [PATCH 443/862] Add basic binary_sensor support to Shelly (#39365) --- .coveragerc | 1 + homeassistant/components/shelly/__init__.py | 2 +- .../components/shelly/binary_sensor.py | 72 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/shelly/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index dc68403fb99..d159c323f52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -754,6 +754,7 @@ omit = homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py + homeassistant/components/shelly/binary_sensor.py homeassistant/components/shelly/light.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/switch.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 093790644bb..427bd0148f2 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers import ( from .const import DOMAIN -PLATFORMS = ["switch", "light", "sensor"] +PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py new file mode 100644 index 00000000000..236b2a1cb5e --- /dev/null +++ b/homeassistant/components/shelly/binary_sensor.py @@ -0,0 +1,72 @@ +"""Binary sensor for Shelly.""" +import aioshelly + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_SMOKE, + BinarySensorEntity, +) + +from . import ShellyBlockEntity, ShellyDeviceWrapper +from .const import DOMAIN + +SENSORS = { + "dwIsOpened": DEVICE_CLASS_OPENING, + "flood": DEVICE_CLASS_MOISTURE, + "overpower": None, + "smoke": DEVICE_CLASS_SMOKE, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors for device.""" + wrapper = hass.data[DOMAIN][config_entry.entry_id] + sensors = [] + + for block in wrapper.device.blocks: + for attr in SENSORS: + if not hasattr(block, attr): + continue + + sensors.append(ShellySensor(wrapper, block, attr)) + + if sensors: + async_add_entities(sensors) + + +class ShellySensor(ShellyBlockEntity, BinarySensorEntity): + """Switch that controls a relay block on Shelly devices.""" + + def __init__( + self, + wrapper: ShellyDeviceWrapper, + block: aioshelly.Block, + attribute: str, + ) -> None: + """Initialize sensor.""" + super().__init__(wrapper, block) + self.attribute = attribute + device_class = SENSORS[attribute] + + self._device_class = device_class + + @property + def unique_id(self): + """Return unique ID of entity.""" + return f"{super().unique_id}-{self.attribute}" + + @property + def name(self): + """Name of sensor.""" + return f"{self.wrapper.name} - {self.attribute}" + + @property + def is_on(self): + """Return true if sensor state is 1.""" + return bool(getattr(self.block, self.attribute)) + + @property + def device_class(self): + """Device class of sensor.""" + return self._device_class From 0cffba77cffba1836eea48d69879e231d4147c66 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 30 Aug 2020 14:18:35 +0200 Subject: [PATCH 444/862] Add more sensors to the Shelly integration (#39368) --- homeassistant/components/shelly/sensor.py | 45 +++++++++++++++++++---- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 2af6d1d91a9..512c387c561 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -2,15 +2,31 @@ import aioshelly from homeassistant.components import sensor -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE +from homeassistant.const import ( + ELECTRICAL_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + UNIT_PERCENTAGE, + VOLT, +) from homeassistant.helpers.entity import Entity from . import ShellyBlockEntity, ShellyDeviceWrapper from .const import DOMAIN SENSORS = { + "battery": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_BATTERY], + "current": [ELECTRICAL_CURRENT_AMPERE, sensor.DEVICE_CLASS_CURRENT], + "deviceTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], + "energy": [ENERGY_KILO_WATT_HOUR, sensor.DEVICE_CLASS_ENERGY], + "energyReturned": [ENERGY_KILO_WATT_HOUR, sensor.DEVICE_CLASS_ENERGY], "extTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], "humidity": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_HUMIDITY], + "overpowerValue": [POWER_WATT, sensor.DEVICE_CLASS_POWER], + "power": [POWER_WATT, sensor.DEVICE_CLASS_POWER], + "voltage": [VOLT, sensor.DEVICE_CLASS_VOLTAGE], } @@ -20,9 +36,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] for block in wrapper.device.blocks: - if block.type != "sensor": - continue - for attr in SENSORS: if not hasattr(block, attr): continue @@ -46,13 +59,18 @@ class ShellySensor(ShellyBlockEntity, Entity): super().__init__(wrapper, block) self.attribute = attribute unit, device_class = SENSORS[attribute] - info = block.info(attribute) + self.info = block.info(attribute) - if info[aioshelly.BLOCK_VALUE_TYPE] == aioshelly.BLOCK_VALUE_TYPE_TEMPERATURE: - if info[aioshelly.BLOCK_VALUE_UNIT] == "C": + if ( + self.info[aioshelly.BLOCK_VALUE_TYPE] + == aioshelly.BLOCK_VALUE_TYPE_TEMPERATURE + ): + if self.info[aioshelly.BLOCK_VALUE_UNIT] == "C": unit = TEMP_CELSIUS else: unit = TEMP_FAHRENHEIT + elif self.info[aioshelly.BLOCK_VALUE_TYPE] == aioshelly.BLOCK_VALUE_TYPE_ENERGY: + unit = ENERGY_KILO_WATT_HOUR self._unit = unit self._device_class = device_class @@ -70,6 +88,19 @@ class ShellySensor(ShellyBlockEntity, Entity): @property def state(self): """Value of sensor.""" + if self.attribute in [ + "deviceTemp", + "extTemp", + "humidity", + "overpowerValue", + "power", + ]: + return round(getattr(self.block, self.attribute), 1) + # Energy unit change from Wmin or Wh to kWh + if self.info[aioshelly.BLOCK_VALUE_UNIT] == "Wmin": + return round(getattr(self.block, self.attribute) / 60 / 1000, 2) + if self.info[aioshelly.BLOCK_VALUE_UNIT] == "Wh": + return round(getattr(self.block, self.attribute) / 1000, 2) return getattr(self.block, self.attribute) @property From c0c6a457bde9b05ccd16866c9ebd7d83936c0ab6 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 14:26:03 +0200 Subject: [PATCH 445/862] Update air_quality to use CoordinatorEntity (#39410) --- homeassistant/components/airly/air_quality.py | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 8ee4e1cd87b..4a3de1e6543 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -6,6 +6,7 @@ from homeassistant.components.air_quality import ( AirQualityEntity, ) from homeassistant.const import CONF_NAME +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_API_ADVICE, @@ -57,12 +58,12 @@ def round_state(func): return _decorator -class AirlyAirQuality(AirQualityEntity): +class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): """Define an Airly air quality.""" def __init__(self, coordinator, name): """Initialize.""" - self.coordinator = coordinator + super().__init__(coordinator) self._name = name self._icon = "mdi:blur" @@ -71,11 +72,6 @@ class AirlyAirQuality(AirQualityEntity): """Return the name.""" return self._name - @property - def should_poll(self): - """Return the polling requirement of the entity.""" - return False - @property def icon(self): """Return the icon.""" @@ -121,11 +117,6 @@ class AirlyAirQuality(AirQualityEntity): "entry_type": "service", } - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - @property def device_state_attributes(self): """Return the state attributes.""" @@ -138,13 +129,3 @@ class AirlyAirQuality(AirQualityEntity): LABEL_PM_10_LIMIT: self.coordinator.data[ATTR_API_PM10_LIMIT], LABEL_PM_10_PERCENT: round(self.coordinator.data[ATTR_API_PM10_PERCENT]), } - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update Airly entity.""" - await self.coordinator.async_request_refresh() From b4db9f615d40840f703c7613fecf5d615dad9e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 30 Aug 2020 15:27:56 +0300 Subject: [PATCH 446/862] Fix huawei_lte duplicate device registry identifiers (#39409) Regression in 3c0f7669337f2fcd55eaaa343cc4b227de294681 Refs https://github.com/home-assistant/core/pull/38925 --- homeassistant/components/huawei_lte/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 8241655d2fa..29db6026cc4 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -385,9 +385,6 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) sw_version = None if router.data.get(KEY_DEVICE_INFORMATION): device_info = router.data[KEY_DEVICE_INFORMATION] - serial_number = device_info.get("SerialNumber") - if serial_number: - device_data["identifiers"] = {(DOMAIN, serial_number)} sw_version = device_info.get("SoftwareVersion") if device_info.get("DeviceName"): device_data["model"] = device_info["DeviceName"] From ba75856f2bc89f25720a0aed8df36f0b93fe77b8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Aug 2020 14:36:00 +0200 Subject: [PATCH 447/862] Do not break Alexa sync when encounter bad entity (#39380) --- .../components/alexa/capabilities.py | 54 ++++++++++++------- homeassistant/components/alexa/entities.py | 31 +++++++---- tests/components/alexa/test_capabilities.py | 23 ++++++++ tests/components/alexa/test_entities.py | 28 ++++++++++ 4 files changed, 107 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index c11e974310c..032aca32e02 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,5 +1,6 @@ """Alexa capabilities.""" import logging +from typing import List, Optional from homeassistant.components import ( cover, @@ -36,6 +37,7 @@ from homeassistant.const import ( STATE_UNKNOWN, STATE_UNLOCKED, ) +from homeassistant.core import State import homeassistant.util.color as color_util import homeassistant.util.dt as dt_util @@ -71,32 +73,32 @@ class AlexaCapability: supported_locales = {"en-US"} - def __init__(self, entity, instance=None): + def __init__(self, entity: State, instance: Optional[str] = None): """Initialize an Alexa capability.""" self.entity = entity self.instance = instance - def name(self): + def name(self) -> str: """Return the Alexa API name of this interface.""" raise NotImplementedError @staticmethod - def properties_supported(): + def properties_supported() -> List[dict]: """Return what properties this entity supports.""" return [] @staticmethod - def properties_proactively_reported(): + def properties_proactively_reported() -> bool: """Return True if properties asynchronously reported.""" return False @staticmethod - def properties_retrievable(): + def properties_retrievable() -> bool: """Return True if properties can be retrieved.""" return False @staticmethod - def properties_non_controllable(): + def properties_non_controllable() -> bool: """Return True if non controllable.""" return None @@ -237,20 +239,34 @@ class AlexaCapability: """Return properties serialized for an API response.""" for prop in self.properties_supported(): prop_name = prop["name"] - prop_value = self.get_property(prop_name) - if prop_value is not None: - result = { - "name": prop_name, - "namespace": self.name(), - "value": prop_value, - "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), - "uncertaintyInMilliseconds": 0, - } - instance = self.instance - if instance is not None: - result["instance"] = instance + try: + prop_value = self.get_property(prop_name) + except UnsupportedProperty: + raise + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unexpected error getting %s.%s property from %s", + self.name(), + prop_name, + self.entity, + ) + prop_value = None - yield result + if prop_value is None: + continue + + result = { + "name": prop_name, + "namespace": self.name(), + "value": prop_value, + "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), + "uncertaintyInMilliseconds": 0, + } + instance = self.instance + if instance is not None: + result["instance"] = instance + + yield result class Alexa(AlexaCapability): diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 9b89f4f15d7..e70da218e47 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -1,6 +1,6 @@ """Alexa entity adapters.""" import logging -from typing import List +from typing import TYPE_CHECKING, List from homeassistant.components import ( alarm_control_panel, @@ -34,7 +34,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import network from homeassistant.util.decorator import Registry @@ -42,6 +42,7 @@ from .capabilities import ( Alexa, AlexaBrightnessController, AlexaCameraStreamController, + AlexaCapability, AlexaChannelController, AlexaColorController, AlexaColorTemperatureController, @@ -72,6 +73,9 @@ from .capabilities import ( ) from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES +if TYPE_CHECKING: + from .config import AbstractConfig + _LOGGER = logging.getLogger(__name__) ENTITY_ADAPTERS = Registry() @@ -203,7 +207,7 @@ class AlexaEntity: The API handlers should manipulate entities only through this interface. """ - def __init__(self, hass, config, entity): + def __init__(self, hass: HomeAssistant, config: "AbstractConfig", entity: State): """Initialize Alexa Entity.""" self.hass = hass self.config = config @@ -246,13 +250,13 @@ class AlexaEntity: """ raise NotImplementedError - def get_interface(self, capability): + def get_interface(self, capability) -> AlexaCapability: """Return the given AlexaInterface. Raises _UnsupportedInterface. """ - def interfaces(self): + def interfaces(self) -> List[AlexaCapability]: """Return a list of supported interfaces. Used for discovery. The list should contain AlexaInterface instances. @@ -280,11 +284,18 @@ class AlexaEntity: } locale = self.config.locale - capabilities = [ - i.serialize_discovery() - for i in self.interfaces() - if locale in i.supported_locales - ] + capabilities = [] + + for i in self.interfaces(): + if locale not in i.supported_locales: + continue + + try: + capabilities.append(i.serialize_discovery()) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error serializing %s discovery for %s", i.name(), self.entity + ) result["capabilities"] = capabilities diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 678a8e74027..2fcc3a236e3 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -33,6 +33,7 @@ from . import ( reported_properties, ) +from tests.async_mock import patch from tests.common import async_mock_service @@ -756,3 +757,25 @@ async def test_report_image_processing(hass): "humanPresenceDetectionState", {"value": "DETECTED"}, ) + + +async def test_get_property_blowup(hass, caplog): + """Test we handle a property blowing up.""" + hass.states.async_set( + "climate.downstairs", + climate.HVAC_MODE_AUTO, + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + with patch( + "homeassistant.components.alexa.capabilities.float", + side_effect=Exception("Boom Fail"), + ): + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_not_has_property("Alexa.ThermostatController", "temperature") + + assert "Boom Fail" in caplog.text diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 8cae4fb9b77..6b48c313fcc 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -3,6 +3,8 @@ from homeassistant.components.alexa import smart_home from . import DEFAULT_CONFIG, get_new_request +from tests.async_mock import patch + async def test_unsupported_domain(hass): """Discovery ignores entities of unknown domains.""" @@ -16,3 +18,29 @@ async def test_unsupported_domain(hass): msg = msg["event"] assert not msg["payload"]["endpoints"] + + +async def test_serialize_discovery_recovers(hass, caplog): + """Test we handle an interface raising unexpectedly during serialize discovery.""" + request = get_new_request("Alexa.Discovery", "Discover") + + hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"}) + + with patch( + "homeassistant.components.alexa.capabilities.AlexaPowerController.serialize_discovery", + side_effect=TypeError, + ): + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request) + + assert "event" in msg + msg = msg["event"] + + interfaces = { + ifc["interface"] for ifc in msg["payload"]["endpoints"][0]["capabilities"] + } + + assert "Alexa.PowerController" not in interfaces + assert ( + f"Error serializing Alexa.PowerController discovery for {hass.states.get('switch.bla')}" + in caplog.text + ) From f7d1cfb62521fab547a9a1eef1e546dcf5b42dc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 07:37:11 -0500 Subject: [PATCH 448/862] Update powerwall to use CoordinatorEntity (#39389) --- .../components/powerwall/binary_sensor.py | 8 ++--- homeassistant/components/powerwall/entity.py | 30 ++----------------- homeassistant/components/powerwall/sensor.py | 6 ++-- 3 files changed, 10 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index b3fe9d977d3..f064616c5ab 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -74,7 +74,7 @@ class PowerWallRunningSensor(PowerWallEntity, BinarySensorEntity): @property def is_on(self): """Get the powerwall running state.""" - return self._coordinator.data[POWERWALL_API_SITEMASTER].running + return self.coordinator.data[POWERWALL_API_SITEMASTER].running class PowerWallConnectedSensor(PowerWallEntity, BinarySensorEntity): @@ -98,7 +98,7 @@ class PowerWallConnectedSensor(PowerWallEntity, BinarySensorEntity): @property def is_on(self): """Get the powerwall connected to tesla state.""" - return self._coordinator.data[POWERWALL_API_SITEMASTER].connected_to_tesla + return self.coordinator.data[POWERWALL_API_SITEMASTER].connected_to_tesla class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): @@ -122,7 +122,7 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): @property def is_on(self): """Grid is online.""" - return self._coordinator.data[POWERWALL_API_GRID_STATUS] == GridStatus.CONNECTED + return self.coordinator.data[POWERWALL_API_GRID_STATUS] == GridStatus.CONNECTED class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): @@ -147,6 +147,6 @@ class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): def is_on(self): """Powerwall is charging.""" # is_sending_to returns true for values greater than 100 watts - return self._coordinator.data[POWERWALL_API_METERS][ + return self.coordinator.data[POWERWALL_API_METERS][ POWERWALL_BATTERY_METER ].is_sending_to() diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index c9cfd124ec6..bcc21615066 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -1,19 +1,18 @@ """The Tesla Powerwall integration base entity.""" -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER, MODEL -class PowerWallEntity(Entity): +class PowerWallEntity(CoordinatorEntity): """Base class for powerwall entities.""" def __init__( self, coordinator, site_info, status, device_type, powerwalls_serial_numbers ): """Initialize the sensor.""" - super().__init__() - self._coordinator = coordinator + super().__init__(coordinator) self._site_info = site_info self._device_type = device_type self._version = status.version @@ -33,26 +32,3 @@ class PowerWallEntity(Entity): device_info["model"] = model device_info["sw_version"] = self._version return device_info - - @property - def available(self): - """Return True if entity is available.""" - return self._coordinator.last_update_success - - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self._coordinator.async_request_refresh() - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index e1e968c0353..ade7746df30 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -89,7 +89,7 @@ class PowerWallChargeSensor(PowerWallEntity): @property def state(self): """Get the current value in percentage.""" - return self._coordinator.data[POWERWALL_API_CHARGE] + return self.coordinator.data[POWERWALL_API_CHARGE] class PowerWallEnergySensor(PowerWallEntity): @@ -134,7 +134,7 @@ class PowerWallEnergySensor(PowerWallEntity): def state(self): """Get the current value in kW.""" return ( - self._coordinator.data[POWERWALL_API_METERS] + self.coordinator.data[POWERWALL_API_METERS] .get(self._meter) .get_power(precision=3) ) @@ -142,7 +142,7 @@ class PowerWallEnergySensor(PowerWallEntity): @property def device_state_attributes(self): """Return the device specific state attributes.""" - meter = self._coordinator.data[POWERWALL_API_METERS].get(self._meter) + meter = self.coordinator.data[POWERWALL_API_METERS].get(self._meter) return { ATTR_FREQUENCY: round(meter.frequency, 1), ATTR_ENERGY_EXPORTED: convert_to_kw(meter.energy_exported), From a4f475245c3af8470337fe0c25b136e58189a607 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 07:39:07 -0500 Subject: [PATCH 449/862] Update griddy to use CoordinatorEntity (#39392) --- homeassistant/components/griddy/sensor.py | 31 +++-------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/griddy/sensor.py b/homeassistant/components/griddy/sensor.py index 9a58e5e3286..acdcefee527 100644 --- a/homeassistant/components/griddy/sensor.py +++ b/homeassistant/components/griddy/sensor.py @@ -2,7 +2,7 @@ import logging from homeassistant.const import ENERGY_KILO_WATT_HOUR -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_LOADZONE, DOMAIN @@ -18,12 +18,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([GriddyPriceSensor(settlement_point, coordinator)], True) -class GriddyPriceSensor(Entity): +class GriddyPriceSensor(CoordinatorEntity): """Representation of an August sensor.""" def __init__(self, settlement_point, coordinator): """Initialize the sensor.""" - self._coordinator = coordinator + super().__init__(coordinator) self._settlement_point = settlement_point @property @@ -46,30 +46,7 @@ class GriddyPriceSensor(Entity): """Device Uniqueid.""" return f"{self._settlement_point}_price_now" - @property - def available(self): - """Return True if entity is available.""" - return self._coordinator.last_update_success - @property def state(self): """Get the current price.""" - return round(float(self._coordinator.data.now.price_cents_kwh), 4) - - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self._coordinator.async_request_refresh() - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) + return round(float(self.coordinator.data.now.price_cents_kwh), 4) From 4e876cb47360aa22c72968241898c5d4c562e0e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 07:40:00 -0500 Subject: [PATCH 450/862] Update myq to use CoordinatorEntity (#39393) --- homeassistant/components/myq/binary_sensor.py | 27 +++++++------------ homeassistant/components/myq/cover.py | 20 +++++--------- 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index aa6c886286c..e5f8fbd057e 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, BinarySensorEntity, ) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY @@ -35,12 +36,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class MyQBinarySensorEntity(BinarySensorEntity): +class MyQBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): """Representation of a MyQ gateway.""" def __init__(self, coordinator, device): """Initialize with API object, device id.""" - self._coordinator = coordinator + super().__init__(coordinator) self._device = device @property @@ -56,7 +57,7 @@ class MyQBinarySensorEntity(BinarySensorEntity): @property def is_on(self): """Return if the device is online.""" - if not self._coordinator.last_update_success: + if not self.coordinator.last_update_success: return False # Not all devices report online so assume True if its missing @@ -64,15 +65,16 @@ class MyQBinarySensorEntity(BinarySensorEntity): MYQ_DEVICE_STATE_ONLINE, True ) + @property + def available(self) -> bool: + """Entity is always available.""" + return True + @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" return self._device.device_id - async def async_update(self): - """Update status of cover.""" - await self._coordinator.async_request_refresh() - @property def device_info(self): """Return the device_info of the device.""" @@ -87,14 +89,3 @@ class MyQBinarySensorEntity(BinarySensorEntity): device_info["model"] = model return device_info - - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 9a44234e747..cccce36f314 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -32,6 +32,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( DOMAIN, @@ -82,12 +83,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class MyQDevice(CoverEntity): +class MyQDevice(CoordinatorEntity, CoverEntity): """Representation of a MyQ cover.""" def __init__(self, coordinator, device): """Initialize with API object, device id.""" - self._coordinator = coordinator + super().__init__(coordinator) self._device = device self._last_action_timestamp = 0 self._scheduled_transition_update = None @@ -108,7 +109,7 @@ class MyQDevice(CoverEntity): @property def available(self): """Return if the device is online.""" - if not self._coordinator.last_update_success: + if not self.coordinator.last_update_success: return False # Not all devices report online so assume True if its missing @@ -173,11 +174,7 @@ class MyQDevice(CoverEntity): async def _async_complete_schedule_update(self, _): """Update status of the cover via coordinator.""" self._scheduled_transition_update = None - await self._coordinator.async_request_refresh() - - async def async_update(self): - """Update status of cover.""" - await self._coordinator.async_request_refresh() + await self.coordinator.async_request_refresh() @property def device_info(self): @@ -204,15 +201,10 @@ class MyQDevice(CoverEntity): self.async_write_ha_state() - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - async def async_added_to_hass(self): """Subscribe to updates.""" self.async_on_remove( - self._coordinator.async_add_listener(self._async_consume_update) + self.coordinator.async_add_listener(self._async_consume_update) ) async def async_will_remove_from_hass(self): From 2525d319a3dcff4a69fb07526bed967abd79caf7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 07:40:26 -0500 Subject: [PATCH 451/862] Update nut to use CoordinatorEntity (#39394) --- homeassistant/components/nut/sensor.py | 29 +++----------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 3c2144a0aee..be98318edfd 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -17,7 +17,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( COORDINATOR, @@ -117,7 +117,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class NUTSensor(Entity): +class NUTSensor(CoordinatorEntity): """Representation of a sensor entity for NUT status values.""" def __init__( @@ -132,7 +132,7 @@ class NUTSensor(Entity): firmware, ): """Initialize the sensor.""" - self._coordinator = coordinator + super().__init__(coordinator) self._type = sensor_type self._manufacturer = manufacturer self._firmware = firmware @@ -200,34 +200,11 @@ class NUTSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self._coordinator.last_update_success - @property def device_state_attributes(self): """Return the sensor attributes.""" return {ATTR_STATE: _format_display_state(self._data.status)} - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self._coordinator.async_request_refresh() - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) - def _format_display_state(status): """Return UPS display state.""" From 21f1875816ffb07ef6d69cec7aaf14df53a46d37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 07:40:55 -0500 Subject: [PATCH 452/862] Update updater to use CoordinatorEntity (#39396) --- .../components/updater/binary_sensor.py | 30 ++----------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 5d45b368500..36e05513d43 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -1,6 +1,7 @@ """Support for Home Assistant Updater binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DOMAIN as UPDATER_DOMAIN @@ -13,13 +14,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([UpdaterBinary(hass.data[UPDATER_DOMAIN])]) -class UpdaterBinary(BinarySensorEntity): +class UpdaterBinary(CoordinatorEntity, BinarySensorEntity): """Representation of an updater binary sensor.""" - def __init__(self, coordinator): - """Initialize the binary sensor.""" - self.coordinator = coordinator - @property def name(self) -> str: """Return the name of the binary sensor, if any.""" @@ -37,16 +34,6 @@ class UpdaterBinary(BinarySensorEntity): return None return self.coordinator.data.update_available - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.coordinator.last_update_success - - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state.""" - return False - @property def device_state_attributes(self) -> dict: """Return the optional state attributes.""" @@ -58,16 +45,3 @@ class UpdaterBinary(BinarySensorEntity): if self.coordinator.data.newest_version: data[ATTR_NEWEST_VERSION] = self.coordinator.data.newest_version return data - - async def async_added_to_hass(self): - """Register update dispatcher.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() From 0f5733ac599c93c7c769759fd2196e34fbb978a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 07:41:18 -0500 Subject: [PATCH 453/862] Update cert_expiry to use CoordinatorEntity (#39397) --- .../components/cert_expiry/sensor.py | 28 ++----------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 55b72bdefcd..ecf6ce18342 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -15,8 +15,8 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt from .const import DEFAULT_PORT, DOMAIN @@ -65,38 +65,14 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(sensors, True) -class CertExpiryEntity(Entity): +class CertExpiryEntity(CoordinatorEntity): """Defines a base Cert Expiry entity.""" - def __init__(self, coordinator): - """Initialize the Cert Expiry entity.""" - self.coordinator = coordinator - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update Cert Expiry entity.""" - await self.coordinator.async_request_refresh() - - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - @property def icon(self): """Icon to use in the frontend, if any.""" return "mdi:certificate" - @property - def should_poll(self): - """Return the polling requirement of the entity.""" - return False - @property def device_state_attributes(self): """Return additional sensor state attributes.""" From 4425c3aea217175d3ebac92e19e0c6427ed6ec7c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 14:41:39 +0200 Subject: [PATCH 454/862] Update airly to use CoordinatorEntity (#39413) --- homeassistant/components/airly/sensor.py | 26 +++--------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 916405be2a5..8d016c4e60b 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -11,7 +11,7 @@ from homeassistant.const import ( TEMP_CELSIUS, UNIT_PERCENTAGE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_API_HUMIDITY, @@ -72,12 +72,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, False) -class AirlySensor(Entity): +class AirlySensor(CoordinatorEntity): """Define an Airly sensor.""" def __init__(self, coordinator, name, kind): """Initialize.""" - self.coordinator = coordinator + super().__init__(coordinator) self._name = name self.kind = kind self._device_class = None @@ -91,11 +91,6 @@ class AirlySensor(Entity): """Return the name.""" return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" - @property - def should_poll(self): - """Return the polling requirement of the entity.""" - return False - @property def state(self): """Return the state.""" @@ -143,18 +138,3 @@ class AirlySensor(Entity): def unit_of_measurement(self): """Return the unit the value is expressed in.""" return SENSOR_TYPES[self.kind][ATTR_UNIT] - - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update Airly entity.""" - await self.coordinator.async_request_refresh() From 108e2ec1ba54e8ffb311d96c41aba28c39b397c5 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Aug 2020 07:42:31 -0500 Subject: [PATCH 455/862] Update roku to use CoordinatorEntity (#39405) --- homeassistant/components/roku/__init__.py | 31 +++++------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index b02ad6430e9..663823d71b4 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -15,9 +15,12 @@ from homeassistant.const import ATTR_NAME, CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from homeassistant.util.dt import utcnow from .const import ( @@ -149,42 +152,22 @@ class RokuDataUpdateCoordinator(DataUpdateCoordinator[Device]): raise UpdateFailed(f"Invalid response from API: {error}") from error -class RokuEntity(Entity): +class RokuEntity(CoordinatorEntity): """Defines a base Roku entity.""" def __init__( self, *, device_id: str, name: str, coordinator: RokuDataUpdateCoordinator ) -> None: """Initialize the Roku entity.""" + super().__init__(coordinator) self._device_id = device_id self._name = name - self.coordinator = coordinator - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.coordinator.last_update_success @property def name(self) -> str: """Return the name of the entity.""" return self._name - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False - - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Update an Roku entity.""" - await self.coordinator.async_request_refresh() - @property def device_info(self) -> Dict[str, Any]: """Return device information about this Roku device.""" From b3ec3d0baf8832288e28124236edafd1f1d22885 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 14:42:45 +0200 Subject: [PATCH 456/862] Update atag to use CoordinatorEntity (#39414) --- homeassistant/components/atag/__init__.py | 31 +++++------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index e5d06c08756..7489ada3341 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -12,8 +12,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, asyncio from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) _LOGGER = logging.getLogger(__name__) @@ -85,12 +88,12 @@ async def async_unload_entry(hass, entry): return unload_ok -class AtagEntity(Entity): +class AtagEntity(CoordinatorEntity): """Defines a base Atag entity.""" def __init__(self, coordinator: AtagDataUpdateCoordinator, atag_id: str) -> None: """Initialize the Atag entity.""" - self.coordinator = coordinator + super().__init__(coordinator) self._id = atag_id self._name = DOMAIN.title() @@ -113,27 +116,7 @@ class AtagEntity(Entity): """Return the name of the entity.""" return self._name - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False - - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - @property def unique_id(self): """Return a unique ID to use for this entity.""" return f"{self.coordinator.atag.id}-{self._id}" - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update Atag entity.""" - await self.coordinator.async_request_refresh() From 854e33025b2dcdb3dc0e8054ba5dd8862cbf5040 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 07:57:02 -0500 Subject: [PATCH 457/862] Update nexia to use CoordinatorEntity (#39391) Co-authored-by: Paulus Schoutsen --- homeassistant/components/nexia/climate.py | 7 ------- homeassistant/components/nexia/entity.py | 23 +++-------------------- homeassistant/components/nexia/scene.py | 2 +- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index b22b185a44c..5085242e6d7 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -458,10 +458,3 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): Update a single zone. """ dispatcher_send(self.hass, f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}") - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 33962bb11c0..7820ebb6216 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -2,7 +2,7 @@ from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTRIBUTION, @@ -13,20 +13,14 @@ from .const import ( ) -class NexiaEntity(Entity): +class NexiaEntity(CoordinatorEntity): """Base class for nexia entities.""" def __init__(self, coordinator, name, unique_id): """Initialize the entity.""" - super().__init__() + super().__init__(coordinator) self._unique_id = unique_id self._name = name - self._coordinator = coordinator - - @property - def available(self): - """Return True if entity is available.""" - return self._coordinator.last_update_success @property def unique_id(self): @@ -45,17 +39,6 @@ class NexiaEntity(Entity): ATTR_ATTRIBUTION: ATTRIBUTION, } - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) - class NexiaThermostatEntity(NexiaEntity): """Base class for nexia devices attached to a thermostat.""" diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index 1700de3f059..d3a6691d59e 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -57,6 +57,6 @@ class NexiaAutomationScene(NexiaEntity, Scene): await self.hass.async_add_executor_job(self._automation.activate) async def refresh_callback(_): - await self._coordinator.async_refresh() + await self.coordinator.async_refresh() async_call_later(self.hass, SCENE_ACTIVATION_TIME, refresh_callback) From ab7b42c022c9128d696026296c9cc5e393a81fb2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Aug 2020 15:19:56 +0200 Subject: [PATCH 458/862] Google: Recover from an entity raising while serializing query (#39381) Co-authored-by: Joakim Plate --- .../components/google_assistant/smart_home.py | 6 +- .../google_assistant/test_smart_home.py | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 50ef200d171..a9a97f047e9 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -137,7 +137,11 @@ async def async_devices_query(hass, data, payload): continue entity = GoogleEntity(hass, data.config, state) - devices[devid] = entity.query_serialize() + try: + devices[devid] = entity.query_serialize() + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error serializing query for %s", state) + devices[devid] = {"online": False} return {"devices": devices} diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 521f52a99eb..cf25c79efdb 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1188,3 +1188,59 @@ async def test_sync_message_recovery(hass, caplog): } assert "Error serializing light.bad_light" in caplog.text + + +async def test_query_recover(hass, caplog): + """Test that we recover if an entity raises during query.""" + + hass.states.async_set( + "light.good", + "on", + { + "supported_features": hass.components.light.SUPPORT_BRIGHTNESS, + "brightness": 50, + }, + ) + hass.states.async_set( + "light.bad", + "on", + { + "supported_features": hass.components.light.SUPPORT_BRIGHTNESS, + "brightness": "shoe", + }, + ) + + result = await sh.async_handle_message( + hass, + BASIC_CONFIG, + "test-agent", + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.QUERY", + "payload": { + "devices": [ + {"id": "light.good"}, + {"id": "light.bad"}, + ] + }, + } + ], + }, + const.SOURCE_CLOUD, + ) + + assert ( + f"Unexpected error serializing query for {hass.states.get('light.bad')}" + in caplog.text + ) + assert result == { + "requestId": REQ_ID, + "payload": { + "devices": { + "light.bad": {"online": False}, + "light.good": {"on": True, "online": True, "brightness": 19}, + } + }, + } From 3d1ff5b8d0e66ef435c030eeee6e24d54abac0c4 Mon Sep 17 00:00:00 2001 From: Andrew Marks Date: Sun, 30 Aug 2020 09:26:11 -0400 Subject: [PATCH 459/862] Add sharkiq integration for Shark IQ robot vacuums (#38272) Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/sharkiq/__init__.py | 119 ++++++++ .../components/sharkiq/config_flow.py | 107 +++++++ homeassistant/components/sharkiq/const.py | 11 + .../components/sharkiq/manifest.json | 9 + homeassistant/components/sharkiq/strings.json | 20 ++ .../components/sharkiq/translations/en.json | 27 ++ .../components/sharkiq/update_coordinator.py | 100 ++++++ homeassistant/components/sharkiq/vacuum.py | 286 ++++++++++++++++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/sharkiq/__init__.py | 1 + tests/components/sharkiq/const.py | 73 +++++ tests/components/sharkiq/test_config_flow.py | 138 +++++++++ tests/components/sharkiq/test_shark_iq.py | 283 +++++++++++++++++ 17 files changed, 1183 insertions(+) create mode 100644 homeassistant/components/sharkiq/__init__.py create mode 100644 homeassistant/components/sharkiq/config_flow.py create mode 100644 homeassistant/components/sharkiq/const.py create mode 100644 homeassistant/components/sharkiq/manifest.json create mode 100644 homeassistant/components/sharkiq/strings.json create mode 100644 homeassistant/components/sharkiq/translations/en.json create mode 100644 homeassistant/components/sharkiq/update_coordinator.py create mode 100644 homeassistant/components/sharkiq/vacuum.py create mode 100644 tests/components/sharkiq/__init__.py create mode 100644 tests/components/sharkiq/const.py create mode 100644 tests/components/sharkiq/test_config_flow.py create mode 100644 tests/components/sharkiq/test_shark_iq.py diff --git a/.coveragerc b/.coveragerc index d159c323f52..d7d8880fdf0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -751,6 +751,7 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py + homeassistant/components/sharkiq/vacuum.py homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 41914c61c67..bfaf15a2ca1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -366,6 +366,7 @@ homeassistant/components/sentry/* @dcramer @frenck homeassistant/components/serial/* @fabaff homeassistant/components/seven_segments/* @fabaff homeassistant/components/seventeentrack/* @bachya +homeassistant/components/sharkiq/* @ajmarks homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shelly/* @balloob homeassistant/components/shiftr/* @fabaff diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py new file mode 100644 index 00000000000..09e6f4e6899 --- /dev/null +++ b/homeassistant/components/sharkiq/__init__.py @@ -0,0 +1,119 @@ +"""Shark IQ Integration.""" + +import asyncio + +import async_timeout +from sharkiqpy import ( + AylaApi, + SharkIqAuthError, + SharkIqAuthExpiringError, + SharkIqNotAuthedError, + get_ayla_api, +) +import voluptuous as vol + +from homeassistant import exceptions +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import API_TIMEOUT, COMPONENTS, DOMAIN, LOGGER +from .update_coordinator import SharkIqUpdateCoordinator + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +async def async_setup(hass, config): + """Set up the sharkiq environment.""" + hass.data.setdefault(DOMAIN, {}) + if DOMAIN not in config: + return True + + +async def async_connect_or_timeout(ayla_api: AylaApi) -> bool: + """Connect to vacuum.""" + try: + with async_timeout.timeout(API_TIMEOUT): + LOGGER.debug("Initialize connection to Ayla networks API") + await ayla_api.async_sign_in() + except SharkIqAuthError as exc: + LOGGER.error("Authentication error connecting to Shark IQ api", exc_info=exc) + return False + except asyncio.TimeoutError as exc: + LOGGER.error("Timeout expired", exc_info=exc) + raise CannotConnect from exc + + return True + + +async def async_setup_entry(hass, config_entry): + """Initialize the sharkiq platform via config entry.""" + ayla_api = get_ayla_api( + username=config_entry.data[CONF_USERNAME], + password=config_entry.data[CONF_PASSWORD], + websession=hass.helpers.aiohttp_client.async_get_clientsession(), + ) + + try: + if not await async_connect_or_timeout(ayla_api): + return False + except CannotConnect as exc: + raise exceptions.ConfigEntryNotReady from exc + + shark_vacs = await ayla_api.async_get_devices(False) + device_names = ", ".join([d.name for d in shark_vacs]) + LOGGER.debug("Found %d Shark IQ device(s): %s", len(device_names), device_names) + coordinator = SharkIqUpdateCoordinator(hass, config_entry, ayla_api, shark_vacs) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise exceptions.ConfigEntryNotReady + + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + for component in COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + return True + + +async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator): + """Disconnect to vacuum.""" + LOGGER.debug("Disconnecting from Ayla Api") + with async_timeout.timeout(5): + try: + await coordinator.ayla_api.async_sign_out() + except (SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError): + pass + return True + + +async def async_update_options(hass, config_entry): + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in COMPONENTS + ] + ) + ) + if unload_ok: + domain_data = hass.data[DOMAIN][config_entry.entry_id] + try: + await async_disconnect_or_timeout(coordinator=domain_data) + except SharkIqAuthError: + pass + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py new file mode 100644 index 00000000000..235f09b11da --- /dev/null +++ b/homeassistant/components/sharkiq/config_flow.py @@ -0,0 +1,107 @@ +"""Config flow for Shark IQ integration.""" + +import asyncio +from typing import Dict, Optional + +import aiohttp +import async_timeout +from sharkiqpy import SharkIqAuthError, get_ayla_api +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import DOMAIN, LOGGER # pylint:disable=unused-import + +SHARKIQ_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + ayla_api = get_ayla_api( + username=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + websession=hass.helpers.aiohttp_client.async_get_clientsession(hass), + ) + + try: + with async_timeout.timeout(10): + LOGGER.debug("Initialize connection to Ayla networks API") + await ayla_api.async_sign_in() + except (asyncio.TimeoutError, aiohttp.ClientError): + raise CannotConnect + except SharkIqAuthError: + raise InvalidAuth + + # Return info that you want to store in the config entry. + return {"title": data[CONF_USERNAME]} + + +class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Shark IQ.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _async_validate_input(self, user_input): + """Validate form input.""" + errors = {} + info = None + + if user_input is not None: + # noinspection PyBroadException + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + return info, errors + + async def async_step_user(self, user_input: Optional[Dict] = None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + info, errors = await self._async_validate_input(user_input) + if info: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=SHARKIQ_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, user_input: Optional[dict] = None): + """Handle re-auth if login is invalid.""" + errors = {} + + if user_input is not None: + _, errors = await self._async_validate_input(user_input) + + if not errors: + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + self.hass.config_entries.async_update_entry( + entry, data=user_input + ) + + return self.async_abort(reason="reauth_successful") + + if errors["base"] != "invalid_auth": + return self.async_abort(reason=errors["base"]) + + return self.async_show_form( + step_id="reauth", data_schema=SHARKIQ_SCHEMA, errors=errors, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/sharkiq/const.py b/homeassistant/components/sharkiq/const.py new file mode 100644 index 00000000000..9160fc710a2 --- /dev/null +++ b/homeassistant/components/sharkiq/const.py @@ -0,0 +1,11 @@ +"""Shark IQ Constants.""" + +from datetime import timedelta +import logging + +API_TIMEOUT = 20 +COMPONENTS = ["vacuum"] +DOMAIN = "sharkiq" +LOGGER = logging.getLogger(__package__) +SHARK = "Shark" +UPDATE_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/sharkiq/manifest.json b/homeassistant/components/sharkiq/manifest.json new file mode 100644 index 00000000000..8aa734ce28a --- /dev/null +++ b/homeassistant/components/sharkiq/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "sharkiq", + "name": "Shark IQ", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sharkiq", + "requirements": ["sharkiqpy==0.1.8"], + "dependencies": [], + "codeowners": ["@ajmarks"] +} diff --git a/homeassistant/components/sharkiq/strings.json b/homeassistant/components/sharkiq/strings.json new file mode 100644 index 00000000000..fe1a2125529 --- /dev/null +++ b/homeassistant/components/sharkiq/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" + } + } +} diff --git a/homeassistant/components/sharkiq/translations/en.json b/homeassistant/components/sharkiq/translations/en.json new file mode 100644 index 00000000000..3bd9bb2e46e --- /dev/null +++ b/homeassistant/components/sharkiq/translations/en.json @@ -0,0 +1,27 @@ +{ + "title": "Shark IQ", + "config": { + "step": { + "init": { + "data": { + "username": "Username", + "password": "Password" + } + }, + "user": { + "data": { + "username": "Username", + "password": "Password" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py new file mode 100644 index 00000000000..b19f12eb403 --- /dev/null +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -0,0 +1,100 @@ +"""Data update coordinator for shark iq vacuums.""" + +from typing import Dict, List, Set + +from async_timeout import timeout +from sharkiqpy import ( + AylaApi, + SharkIqAuthError, + SharkIqAuthExpiringError, + SharkIqNotAuthedError, + SharkIqVacuum, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import API_TIMEOUT, DOMAIN, LOGGER, UPDATE_INTERVAL + + +class SharkIqUpdateCoordinator(DataUpdateCoordinator): + """Define a wrapper class to update Shark IQ data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + ayla_api: AylaApi, + shark_vacs: List[SharkIqVacuum], + ) -> None: + """Set up the SharkIqUpdateCoordinator class.""" + self.ayla_api = ayla_api + self.shark_vacs: Dict[SharkIqVacuum] = { + sharkiq.serial_number: sharkiq for sharkiq in shark_vacs + } + self._config_entry = config_entry + self._online_dsns = set() + + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + + @property + def online_dsns(self) -> Set[str]: + """Get the set of all online DSNs.""" + return self._online_dsns + + def device_is_online(self, dsn: str) -> bool: + """Return the online state of a given vacuum dsn.""" + return dsn in self._online_dsns + + @staticmethod + async def _async_update_vacuum(sharkiq: SharkIqVacuum) -> None: + """Asynchronously update the data for a single vacuum.""" + dsn = sharkiq.serial_number + LOGGER.info("Updating sharkiq data for device DSN %s", dsn) + with timeout(API_TIMEOUT): + await sharkiq.async_update() + + async def _async_update_data(self) -> bool: + """Update data device by device.""" + try: + all_vacuums = await self.ayla_api.async_list_devices() + self._online_dsns = { + v["dsn"] + for v in all_vacuums + if v["connection_status"] == "Online" and v["dsn"] in self.shark_vacs + } + + LOGGER.info("Updating sharkiq data") + for dsn in self._online_dsns: + await self._async_update_vacuum(self.shark_vacs[dsn]) + except ( + SharkIqAuthError, + SharkIqNotAuthedError, + SharkIqAuthExpiringError, + ) as err: + LOGGER.exception("Bad auth state", exc_info=err) + flow_context = { + "source": "reauth", + "unique_id": self._config_entry.unique_id, + } + + matching_flows = [ + flow + for flow in self.hass.config_entries.flow.async_progress() + if flow["context"] == flow_context + ] + + if not matching_flows: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DOMAIN, context=flow_context, data=self._config_entry.data, + ) + ) + + raise UpdateFailed(err) + except Exception as err: # pylint: disable=broad-except + LOGGER.exception("Unexpected error updating SharkIQ", exc_info=err) + raise UpdateFailed(err) + + return True diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py new file mode 100644 index 00000000000..5be95fdb516 --- /dev/null +++ b/homeassistant/components/sharkiq/vacuum.py @@ -0,0 +1,286 @@ +"""Shark IQ Wrapper.""" + + +import logging +from typing import Dict, Iterable, Optional + +from sharkiqpy import OperatingModes, PowerModes, Properties, SharkIqVacuum + +from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, + SUPPORT_BATTERY, + SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, + SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STATUS, + SUPPORT_STOP, + StateVacuumEntity, +) + +from .const import DOMAIN, SHARK +from .update_coordinator import SharkIqUpdateCoordinator + +LOGGER = logging.getLogger(__name__) + +# Supported features +SUPPORT_SHARKIQ = ( + SUPPORT_BATTERY + | SUPPORT_FAN_SPEED + | SUPPORT_PAUSE + | SUPPORT_RETURN_HOME + | SUPPORT_START + | SUPPORT_STATE + | SUPPORT_STATUS + | SUPPORT_STOP + | SUPPORT_LOCATE +) + +OPERATING_STATE_MAP = { + OperatingModes.PAUSE: STATE_PAUSED, + OperatingModes.START: STATE_CLEANING, + OperatingModes.STOP: STATE_IDLE, + OperatingModes.RETURN: STATE_RETURNING, +} + +FAN_SPEEDS_MAP = { + "Eco": PowerModes.ECO, + "Normal": PowerModes.NORMAL, + "Max": PowerModes.MAX, +} + +STATE_RECHARGING_TO_RESUME = "recharging_to_resume" + +# Attributes to expose +ATTR_ERROR_CODE = "last_error_code" +ATTR_ERROR_MSG = "last_error_message" +ATTR_LOW_LIGHT = "low_light" +ATTR_RECHARGE_RESUME = "recharge_and_resume" +ATTR_RSSI = "rssi" + + +class SharkVacuumEntity(StateVacuumEntity): + """Shark IQ vacuum entity.""" + + def __init__(self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator): + """Create a new SharkVacuumEntity.""" + if sharkiq.serial_number not in coordinator.shark_vacs: + raise RuntimeError( + f"Shark IQ robot {sharkiq.serial_number} is not known to the coordinator" + ) + self.coordinator = coordinator + self.sharkiq = sharkiq + + @property + def should_poll(self): + """Don't poll this entity. Polling is done via the coordinator.""" + return False + + def clean_spot(self, **kwargs): + """Clean a spot. Not yet implemented.""" + raise NotImplementedError() + + def send_command(self, command, params=None, **kwargs): + """Send a command to the vacuum. Not yet implemented.""" + raise NotImplementedError() + + @property + def is_online(self) -> bool: + """Tell us if the device is online.""" + return self.coordinator.device_is_online(self.sharkiq.serial_number) + + @property + def name(self) -> str: + """Device name.""" + return self.sharkiq.name + + @property + def serial_number(self) -> str: + """Vacuum API serial number (DSN).""" + return self.sharkiq.serial_number + + @property + def model(self) -> str: + """Vacuum model number.""" + if self.sharkiq.vac_model_number: + return self.sharkiq.vac_model_number + return self.sharkiq.oem_model_number + + @property + def device_info(self) -> Dict: + """Device info dictionary.""" + return { + "identifiers": {(DOMAIN, self.serial_number)}, + "name": self.name, + "manufacturer": SHARK, + "model": self.model, + "sw_version": self.sharkiq.get_property_value( + Properties.ROBOT_FIRMWARE_VERSION + ), + } + + @property + def supported_features(self) -> int: + """Flag vacuum cleaner robot features that are supported.""" + return SUPPORT_SHARKIQ + + @property + def is_docked(self) -> Optional[bool]: + """Is vacuum docked.""" + return self.sharkiq.get_property_value(Properties.DOCKED_STATUS) + + @property + def error_code(self) -> Optional[int]: + """Return the last observed error code (or None).""" + return self.sharkiq.error_code + + @property + def error_message(self) -> Optional[str]: + """Return the last observed error message (or None).""" + if not self.error_code: + return None + return self.sharkiq.error_text + + @property + def operating_mode(self) -> Optional[str]: + """Operating mode..""" + op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE) + return OPERATING_STATE_MAP.get(op_mode) + + @property + def recharging_to_resume(self) -> Optional[int]: + """Return True if vacuum set to recharge and resume cleaning.""" + return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME) + + @property + def state(self): + """ + Get the current vacuum state. + + NB: Currently, we do not return an error state because they can be very, very stale. + In the app, these are (usually) handled by showing the robot as stopped and sending the + user a notification. + """ + if self.recharging_to_resume: + return STATE_RECHARGING_TO_RESUME + if self.is_docked: + return STATE_DOCKED + return self.operating_mode + + @property + def unique_id(self) -> str: + """Return the unique id of the vacuum cleaner.""" + return self.serial_number + + @property + def available(self) -> bool: + """Determine if the sensor is available based on API results.""" + # If the last update was successful... + return self.coordinator.last_update_success and self.is_online + + @property + def battery_level(self): + """Get the current battery level.""" + return self.sharkiq.get_property_value(Properties.BATTERY_CAPACITY) + + async def async_update(self): + """Update the known properties asynchronously.""" + await self.coordinator.async_request_refresh() + + async def async_added_to_hass(self) -> None: + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_return_to_base(self, **kwargs): + """Have the device return to base.""" + await self.sharkiq.async_set_operating_mode(OperatingModes.RETURN) + await self.coordinator.async_refresh() + + async def async_pause(self): + """Pause the cleaning task.""" + await self.sharkiq.async_set_operating_mode(OperatingModes.PAUSE) + await self.coordinator.async_refresh() + + async def async_start(self): + """Start the device.""" + await self.sharkiq.async_set_operating_mode(OperatingModes.START) + await self.coordinator.async_refresh() + + async def async_stop(self, **kwargs): + """Stop the device.""" + await self.sharkiq.async_set_operating_mode(OperatingModes.STOP) + await self.coordinator.async_refresh() + + async def async_locate(self, **kwargs): + """Cause the device to generate a loud chirp.""" + await self.sharkiq.async_find_device() + + @property + def fan_speed(self) -> str: + """Return the current fan speed.""" + fan_speed = None + speed_level = self.sharkiq.get_property_value(Properties.POWER_MODE) + for k, val in FAN_SPEEDS_MAP.items(): + if val == speed_level: + fan_speed = k + return fan_speed + + async def async_set_fan_speed(self, fan_speed: str, **kwargs): + """Set the fan speed.""" + await self.sharkiq.async_set_property_value( + Properties.POWER_MODE, FAN_SPEEDS_MAP.get(fan_speed.capitalize()) + ) + await self.coordinator.async_refresh() + + @property + def fan_speed_list(self): + """Get the list of available fan speed steps of the vacuum cleaner.""" + return list(FAN_SPEEDS_MAP.keys()) + + # Various attributes we want to expose + @property + def recharge_resume(self) -> Optional[bool]: + """Recharge and resume mode active.""" + return self.sharkiq.get_property_value(Properties.RECHARGE_RESUME) + + @property + def rssi(self) -> Optional[int]: + """Get the WiFi RSSI.""" + return self.sharkiq.get_property_value(Properties.RSSI) + + @property + def low_light(self): + """Let us know if the robot is operating in low-light mode.""" + return self.sharkiq.get_property_value(Properties.LOW_LIGHT_MISSION) + + @property + def device_state_attributes(self) -> Dict: + """Return a dictionary of device state attributes specific to sharkiq.""" + data = { + ATTR_ERROR_CODE: self.error_code, + ATTR_ERROR_MSG: self.sharkiq.error_text, + ATTR_LOW_LIGHT: self.low_light, + ATTR_RECHARGE_RESUME: self.recharge_resume, + } + return data + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Shark IQ vacuum cleaner.""" + coordinator: SharkIqUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + devices: Iterable["SharkIqVacuum"] = coordinator.shark_vacs.values() + device_names = [d.name for d in devices] + LOGGER.debug( + "Found %d Shark IQ device(s): %s", + len(device_names), + ", ".join([d.name for d in devices]), + ) + async_add_entities([SharkVacuumEntity(d, coordinator) for d in devices]) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d8533554ea2..bc5470188b3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -152,6 +152,7 @@ FLOWS = [ "samsungtv", "sense", "sentry", + "sharkiq", "shelly", "shopping_list", "simplisafe", diff --git a/requirements_all.txt b/requirements_all.txt index 1872b1e4d23..40670f1324c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1958,6 +1958,9 @@ sense_energy==0.7.2 # homeassistant.components.sentry sentry-sdk==0.16.5 +# homeassistant.components.sharkiq +sharkiqpy==0.1.8 + # homeassistant.components.aquostv sharp_aquos_rc==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39eb8861c61..42240019185 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -907,6 +907,9 @@ sense_energy==0.7.2 # homeassistant.components.sentry sentry-sdk==0.16.5 +# homeassistant.components.sharkiq +sharkiqpy==0.1.8 + # homeassistant.components.sighthound simplehound==0.3 diff --git a/tests/components/sharkiq/__init__.py b/tests/components/sharkiq/__init__.py new file mode 100644 index 00000000000..d6f1072e9a8 --- /dev/null +++ b/tests/components/sharkiq/__init__.py @@ -0,0 +1 @@ +"""Tests for the Shark IQ integration.""" diff --git a/tests/components/sharkiq/const.py b/tests/components/sharkiq/const.py new file mode 100644 index 00000000000..392b4a863d3 --- /dev/null +++ b/tests/components/sharkiq/const.py @@ -0,0 +1,73 @@ +"""Constants used in shark iq tests.""" + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +# Dummy device dict of the form returned by AylaApi.list_devices() +SHARK_DEVICE_DICT = { + "product_name": "Sharknado", + "model": "AY001MRT1", + "dsn": "AC000Wxxxxxxxxx", + "oem_model": "RV1000A", + "sw_version": "devd 1.7 2020-05-13 11:50:36", + "template_id": 99999, + "mac": "ffffffffffff", + "unique_hardware_id": None, + "lan_ip": "192.168.0.123", + "connected_at": "2020-07-31T08:03:05Z", + "key": 26517570, + "lan_enabled": False, + "has_properties": True, + "product_class": None, + "connection_status": "Online", + "lat": "99.9999", + "lng": "-99.9999", + "locality": "99999", + "device_type": "Wifi", +} + +# Dummy response for get_metadata +SHARK_METADATA_DICT = [ + { + "datum": { + "created_at": "2019-12-02T02:13:12Z", + "from_template": False, + "key": "sharkDeviceMobileData", + "updated_at": "2019-12-02T02:13:12Z", + "value": '{"vacModelNumber":"RV1001AE","vacSerialNumber":"S26xxxxxxxxx"}', + "dsn": "AC000Wxxxxxxxxx", + } + } +] + +# Dummy shark.properties_full for testing. NB: this only includes those properties in the tests +SHARK_PROPERTIES_DICT = { + "Battery_Capacity": {"base_type": "integer", "read_only": True, "value": 50}, + "Charging_Status": {"base_type": "boolean", "read_only": True, "value": 0}, + "CleanComplete": {"base_type": "boolean", "read_only": True, "value": 0}, + "Cleaning_Statistics": {"base_type": "file", "read_only": True, "value": None}, + "DockedStatus": {"base_type": "boolean", "read_only": True, "value": 0}, + "Error_Code": {"base_type": "integer", "read_only": True, "value": 7}, + "Evacuating": {"base_type": "boolean", "read_only": True, "value": 1}, + "Find_Device": {"base_type": "boolean", "read_only": False, "value": 0}, + "LowLightMission": {"base_type": "boolean", "read_only": True, "value": 0}, + "Nav_Module_FW_Version": { + "base_type": "string", + "read_only": True, + "value": "V3.4.11-20191015", + }, + "Operating_Mode": {"base_type": "integer", "read_only": False, "value": 2}, + "Power_Mode": {"base_type": "integer", "read_only": False, "value": 1}, + "RSSI": {"base_type": "integer", "read_only": True, "value": -46}, + "Recharge_Resume": {"base_type": "boolean", "read_only": False, "value": 1}, + "Recharging_To_Resume": {"base_type": "boolean", "read_only": True, "value": 0}, + "Robot_Firmware_Version": { + "base_type": "string", + "read_only": True, + "value": "Dummy Firmware 1.0", + }, +} + +TEST_USERNAME = "test-username" +TEST_PASSWORD = "test-password" +UNIQUE_ID = "foo@bar.com" +CONFIG = {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD} diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py new file mode 100644 index 00000000000..5cf6bee25c4 --- /dev/null +++ b/tests/components/sharkiq/test_config_flow.py @@ -0,0 +1,138 @@ +"""Test the Shark IQ config flow.""" +import aiohttp +from sharkiqpy import SharkIqAuthError + +from homeassistant import config_entries, setup +from homeassistant.components.sharkiq.const import DOMAIN + +from .const import CONFIG, TEST_PASSWORD, TEST_USERNAME, UNIQUE_ID + +from tests.async_mock import MagicMock, PropertyMock, patch +from tests.common import MockConfigEntry + + +def _create_mocked_ayla(connect=None): + """Create a mocked AylaApi object.""" + mocked_ayla = MagicMock() + type(mocked_ayla).sign_in = PropertyMock(side_effect=connect) + type(mocked_ayla).async_sign_in = PropertyMock(side_effect=connect) + return mocked_ayla + + +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("sharkiqpy.AylaApi.async_sign_in", return_value=True), patch( + "homeassistant.components.sharkiq.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.sharkiq.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == f"{TEST_USERNAME:s}" + assert result2["data"] == { + "username": TEST_USERNAME, + "password": TEST_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_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mocked_ayla = _create_mocked_ayla(connect=SharkIqAuthError) + + with patch( + "homeassistant.components.sharkiq.config_flow.get_ayla_api", + return_value=mocked_ayla, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mocked_ayla = _create_mocked_ayla(connect=aiohttp.ClientError) + + with patch( + "homeassistant.components.sharkiq.config_flow.get_ayla_api", + return_value=mocked_ayla, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_other_error(hass): + """Test we handle other errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mocked_ayla = _create_mocked_ayla(connect=TypeError) + + with patch( + "homeassistant.components.sharkiq.config_flow.get_ayla_api", + return_value=mocked_ayla, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_reauth(hass): + """Test reauth flow.""" + with patch( + "homeassistant.components.sharkiq.vacuum.async_setup_entry", return_value=True, + ), patch("sharkiqpy.AylaApi.async_sign_in", return_value=True): + mock_config = MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG) + mock_config.add_to_hass(hass) + hass.config_entries.async_update_entry(mock_config, data=CONFIG) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG + ) + + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + + with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=SharkIqAuthError): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG, + ) + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_auth"} + + with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=RuntimeError): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG, + ) + + assert result["type"] == "abort" + assert result["reason"] == "unknown" diff --git a/tests/components/sharkiq/test_shark_iq.py b/tests/components/sharkiq/test_shark_iq.py new file mode 100644 index 00000000000..48f623d4509 --- /dev/null +++ b/tests/components/sharkiq/test_shark_iq.py @@ -0,0 +1,283 @@ +"""Test the Shark IQ vacuum entity.""" +from copy import deepcopy +import enum +from typing import Dict, List + +from sharkiqpy import AylaApi, Properties, SharkIqAuthError, SharkIqVacuum, get_ayla_api + +from homeassistant.components.sharkiq import SharkIqUpdateCoordinator +from homeassistant.components.sharkiq.vacuum import ( + ATTR_ERROR_CODE, + ATTR_ERROR_MSG, + ATTR_LOW_LIGHT, + ATTR_RECHARGE_RESUME, + STATE_RECHARGING_TO_RESUME, + SharkVacuumEntity, +) +from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, + SUPPORT_BATTERY, + SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, + SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STATUS, + SUPPORT_STOP, +) +from homeassistant.config_entries import ConfigEntriesFlowManager, ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import UpdateFailed + +from .const import ( + SHARK_DEVICE_DICT, + SHARK_METADATA_DICT, + SHARK_PROPERTIES_DICT, + TEST_PASSWORD, + TEST_USERNAME, +) + +from tests.async_mock import MagicMock, patch + +try: + import ujson as json +except ImportError: + import json + + +MockAyla = MagicMock(spec=AylaApi) # pylint: disable=invalid-name + + +def _set_property(self, property_name, value): + """Set a property locally without hitting the API.""" + if isinstance(property_name, enum.Enum): + property_name = property_name.value + if isinstance(value, enum.Enum): + value = value.value + self.properties_full[property_name]["value"] = value + + +async def _async_set_property(self, property_name, value): + """Set a property locally without hitting the API.""" + _set_property(self, property_name, value) + + +def _get_mock_shark_vac(ayla_api: AylaApi) -> SharkIqVacuum: + """Create a crude sharkiq vacuum with mocked properties.""" + shark = SharkIqVacuum(ayla_api, SHARK_DEVICE_DICT) + shark.properties_full = deepcopy(SHARK_PROPERTIES_DICT) + return shark + + +async def _async_list_devices(_) -> List[Dict]: + """Generate a dummy of async_list_devices output.""" + return [SHARK_DEVICE_DICT] + + +@patch.object(SharkIqVacuum, "set_property_value", new=_set_property) +@patch.object(SharkIqVacuum, "async_set_property_value", new=_async_set_property) +async def test_shark_operation_modes(hass: HomeAssistant) -> None: + """Test all of the shark vacuum operation modes.""" + ayla_api = MockAyla() + shark_vac = _get_mock_shark_vac(ayla_api) + coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac]) + shark = SharkVacuumEntity(shark_vac, coordinator) + + # These come from the setup + assert isinstance(shark.is_docked, bool) and not shark.is_docked + assert ( + isinstance(shark.recharging_to_resume, bool) and not shark.recharging_to_resume + ) + # Go through the operation modes while it's "off the dock" + await shark.async_start() + assert shark.operating_mode == shark.state == STATE_CLEANING + await shark.async_pause() + assert shark.operating_mode == shark.state == STATE_PAUSED + await shark.async_stop() + assert shark.operating_mode == shark.state == STATE_IDLE + await shark.async_return_to_base() + assert shark.operating_mode == shark.state == STATE_RETURNING + + # Test the docked modes + await shark.async_stop() + shark.sharkiq.set_property_value(Properties.RECHARGING_TO_RESUME, 1) + shark.sharkiq.set_property_value(Properties.DOCKED_STATUS, 1) + assert isinstance(shark.is_docked, bool) and shark.is_docked + assert isinstance(shark.recharging_to_resume, bool) and shark.recharging_to_resume + assert shark.state == STATE_RECHARGING_TO_RESUME + + shark.sharkiq.set_property_value(Properties.RECHARGING_TO_RESUME, 0) + assert shark.state == STATE_DOCKED + + await shark.async_set_fan_speed("Eco") + assert shark.fan_speed == "Eco" + await shark.async_set_fan_speed("Max") + assert shark.fan_speed == "Max" + await shark.async_set_fan_speed("Normal") + assert shark.fan_speed == "Normal" + + assert set(shark.fan_speed_list) == {"Normal", "Max", "Eco"} + + +@patch.object(SharkIqVacuum, "set_property_value", new=_set_property) +async def test_shark_vac_properties(hass: HomeAssistant) -> None: + """Test all of the shark vacuum property accessors.""" + ayla_api = MockAyla() + shark_vac = _get_mock_shark_vac(ayla_api) + coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac]) + shark = SharkVacuumEntity(shark_vac, coordinator) + + assert shark.name == "Sharknado" + assert shark.serial_number == "AC000Wxxxxxxxxx" + assert shark.model == "RV1000A" + + assert shark.battery_level == 50 + assert shark.fan_speed == "Eco" + shark.sharkiq.set_property_value(Properties.POWER_MODE, 0) + assert shark.fan_speed == "Normal" + assert isinstance(shark.recharge_resume, bool) and shark.recharge_resume + assert isinstance(shark.low_light, bool) and not shark.low_light + + target_state_attributes = { + ATTR_ERROR_CODE: 7, + ATTR_ERROR_MSG: "Cliff sensor is blocked", + ATTR_RECHARGE_RESUME: True, + ATTR_LOW_LIGHT: False, + } + state_json = json.dumps(shark.device_state_attributes, sort_keys=True) + target_json = json.dumps(target_state_attributes, sort_keys=True) + assert state_json == target_json + + assert not shark.should_poll + + +@patch.object(SharkIqVacuum, "set_property_value", new=_set_property) +@patch.object(SharkIqVacuum, "async_set_property_value", new=_async_set_property) +async def test_shark_metadata(hass: HomeAssistant) -> None: + """Test shark properties coming from metadata.""" + ayla_api = MockAyla() + shark_vac = _get_mock_shark_vac(ayla_api) + coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac]) + shark = SharkVacuumEntity(shark_vac, coordinator) + shark.sharkiq._update_metadata( # pylint: disable=protected-access + SHARK_METADATA_DICT + ) + + target_device_info = { + "identifiers": {("sharkiq", "AC000Wxxxxxxxxx")}, + "name": "Sharknado", + "manufacturer": "Shark", + "model": "RV1001AE", + "sw_version": "Dummy Firmware 1.0", + } + state_json = json.dumps(shark.device_info, sort_keys=True) + target_json = json.dumps(target_device_info, sort_keys=True) + assert state_json == target_json + + +def _get_async_update(err=None): + async def _async_update(_) -> bool: + if err is not None: + raise err + return True + + return _async_update + + +@patch.object(AylaApi, "async_list_devices", new=_async_list_devices) +async def test_updates(hass: HomeAssistant) -> None: + """Test the update coordinator update functions.""" + ayla_api = get_ayla_api(TEST_USERNAME, TEST_PASSWORD) + shark_vac = _get_mock_shark_vac(ayla_api) + mock_config = MagicMock(spec=ConfigEntry) + coordinator = SharkIqUpdateCoordinator(hass, mock_config, ayla_api, [shark_vac]) + + with patch.object(SharkIqVacuum, "async_update", new=_get_async_update()): + update_called = ( + await coordinator._async_update_data() # pylint: disable=protected-access + ) + assert update_called + + update_failed = False + with patch.object( + SharkIqVacuum, "async_update", new=_get_async_update(SharkIqAuthError) + ), patch.object(HomeAssistant, "async_create_task"), patch.object( + ConfigEntriesFlowManager, "async_init" + ): + try: + await coordinator._async_update_data() # pylint: disable=protected-access + except UpdateFailed: + update_failed = True + assert update_failed + + +async def test_coordinator_match(hass: HomeAssistant): + """Test that sharkiq-coordinator references work.""" + ayla_api = get_ayla_api(TEST_PASSWORD, TEST_USERNAME) + shark_vac1 = _get_mock_shark_vac(ayla_api) + shark_vac2 = _get_mock_shark_vac(ayla_api) + shark_vac2._dsn = "FOOBAR!" # pylint: disable=protected-access + + coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac1]) + + # The first should succeed, the second should fail + api1 = SharkVacuumEntity(shark_vac1, coordinator) + try: + _ = SharkVacuumEntity(shark_vac2, coordinator) + except RuntimeError: + api2_failed = True + else: + api2_failed = False + assert api2_failed + + coordinator.last_update_success = True + coordinator._online_dsns = set() # pylint: disable=protected-access + assert not api1.is_online + assert not api1.available + + coordinator._online_dsns = { # pylint: disable=protected-access + shark_vac1.serial_number + } + assert api1.is_online + assert api1.available + + coordinator.last_update_success = False + assert not api1.available + + +async def test_simple_properties(hass: HomeAssistant): + """Test that simple properties work as intended.""" + ayla_api = get_ayla_api(TEST_PASSWORD, TEST_USERNAME) + shark_vac1 = _get_mock_shark_vac(ayla_api) + coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac1]) + entity = SharkVacuumEntity(shark_vac1, coordinator) + + assert entity.unique_id == "AC000Wxxxxxxxxx" + + assert entity.supported_features == ( + SUPPORT_BATTERY + | SUPPORT_FAN_SPEED + | SUPPORT_PAUSE + | SUPPORT_RETURN_HOME + | SUPPORT_START + | SUPPORT_STATE + | SUPPORT_STATUS + | SUPPORT_STOP + | SUPPORT_LOCATE + ) + + assert entity.error_code == 7 + assert entity.error_message == "Cliff sensor is blocked" + shark_vac1.properties_full[Properties.ERROR_CODE.value]["value"] = 0 + assert entity.error_code == 0 + assert entity.error_message is None + + assert ( + coordinator.online_dsns + is coordinator._online_dsns # pylint: disable=protected-access + ) From 5a7f1d62c1f6675bee0c88cc820d88076c70eb52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Aug 2020 15:42:33 +0200 Subject: [PATCH 460/862] CoordinatorEntity to call super added_to_hass (#39416) --- homeassistant/helpers/update_coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 987f1d63eee..b17ddcd3dd6 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -211,6 +211,7 @@ class CoordinatorEntity(entity.Entity): async def async_added_to_hass(self) -> None: """When entity is added to hass.""" + await super().async_added_to_hass() self.async_on_remove( self.coordinator.async_add_listener(self.async_write_ha_state) ) From 5dfb043896ffb22a8502743972f049b1063e826e Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Aug 2020 08:47:28 -0500 Subject: [PATCH 461/862] update nzbget to use CoordinatorEntity (#39406) --- homeassistant/components/nzbget/__init__.py | 26 +++------------------ 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 9976d2fffc8..130fa0d55b8 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -16,8 +16,8 @@ from homeassistant.const import ( ) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_SPEED, @@ -168,38 +168,18 @@ async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> await hass.config_entries.async_reload(entry.entry_id) -class NZBGetEntity(Entity): +class NZBGetEntity(CoordinatorEntity): """Defines a base NZBGet entity.""" def __init__( self, *, entry_id: str, name: str, coordinator: NZBGetDataUpdateCoordinator ) -> None: """Initialize the NZBGet entity.""" + super().__init__(coordinator) self._name = name self._entry_id = entry_id - self.coordinator = coordinator - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.coordinator.last_update_success @property def name(self) -> str: """Return the name of the entity.""" return self._name - - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False - - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Request an update from the coordinator of this entity.""" - await self.coordinator.async_request_refresh() From be489e83a14c60d07633ee2e0786c5eebe0996d5 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 15:50:33 +0200 Subject: [PATCH 462/862] Update speedtestdotnet to use CoordinatorEntity (#39404) --- homeassistant/components/speedtestdotnet/sensor.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index d071a226a05..cf0a91a240e 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -4,6 +4,7 @@ import logging from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_BYTES_RECEIVED, @@ -33,13 +34,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities) -class SpeedtestSensor(RestoreEntity): +class SpeedtestSensor(CoordinatorEntity, RestoreEntity): """Implementation of a speedtest.net sensor.""" def __init__(self, coordinator, sensor_type): """Initialize the sensor.""" + super().__init__(coordinator) self._name = SENSOR_TYPES[sensor_type][0] - self.coordinator = coordinator self.type = sensor_type self._unit_of_measurement = SENSOR_TYPES[self.type][1] self._state = None @@ -69,11 +70,6 @@ class SpeedtestSensor(RestoreEntity): """Return icon.""" return ICON - @property - def should_poll(self): - """Return the polling requirement for this sensor.""" - return False - @property def device_state_attributes(self): """Return the state attributes.""" @@ -118,7 +114,3 @@ class SpeedtestSensor(RestoreEntity): self._state = round(self.coordinator.data["download"] / 10 ** 6, 2) elif self.type == "upload": self._state = round(self.coordinator.data["upload"] / 10 ** 6, 2) - - async def async_update(self): - """Request coordinator to update data.""" - await self.coordinator.async_request_refresh() From 4294d107890ca1bc92b2e965314b1b30c1beb03c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 15:56:59 +0200 Subject: [PATCH 463/862] Format sharkiq with black (#39422) --- .../components/sharkiq/config_flow.py | 4 +++- .../components/sharkiq/update_coordinator.py | 4 +++- tests/components/sharkiq/test_config_flow.py | 23 +++++++++++++------ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 235f09b11da..b2b85d6cf36 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -95,7 +95,9 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason=errors["base"]) return self.async_show_form( - step_id="reauth", data_schema=SHARKIQ_SCHEMA, errors=errors, + step_id="reauth", + data_schema=SHARKIQ_SCHEMA, + errors=errors, ) diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index b19f12eb403..c498307ac9d 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -88,7 +88,9 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): if not matching_flows: self.hass.async_create_task( self.hass.config_entries.flow.async_init( - DOMAIN, context=flow_context, data=self._config_entry.data, + DOMAIN, + context=flow_context, + data=self._config_entry.data, ) ) diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index 5cf6bee25c4..0d1612f857b 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -31,10 +31,12 @@ async def test_form(hass): with patch("sharkiqpy.AylaApi.async_sign_in", return_value=True), patch( "homeassistant.components.sharkiq.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.sharkiq.async_setup_entry", return_value=True, + "homeassistant.components.sharkiq.async_setup_entry", + return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG, + result["flow_id"], + CONFIG, ) await hass.async_block_till_done() @@ -61,7 +63,8 @@ async def test_form_invalid_auth(hass): return_value=mocked_ayla, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG, + result["flow_id"], + CONFIG, ) assert result2["type"] == "form" @@ -80,7 +83,8 @@ async def test_form_cannot_connect(hass): return_value=mocked_ayla, ): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG, + result["flow_id"], + CONFIG, ) assert result2["type"] == "form" @@ -109,7 +113,8 @@ async def test_form_other_error(hass): async def test_reauth(hass): """Test reauth flow.""" with patch( - "homeassistant.components.sharkiq.vacuum.async_setup_entry", return_value=True, + "homeassistant.components.sharkiq.vacuum.async_setup_entry", + return_value=True, ), patch("sharkiqpy.AylaApi.async_sign_in", return_value=True): mock_config = MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG) mock_config.add_to_hass(hass) @@ -124,14 +129,18 @@ async def test_reauth(hass): with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=SharkIqAuthError): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG, + DOMAIN, + context={"source": "reauth", "unique_id": UNIQUE_ID}, + data=CONFIG, ) assert result["type"] == "form" assert result["errors"] == {"base": "invalid_auth"} with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=RuntimeError): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG, + DOMAIN, + context={"source": "reauth", "unique_id": UNIQUE_ID}, + data=CONFIG, ) assert result["type"] == "abort" From 64513c8c9a0b814c7a975cfa3b1440d8b719ea19 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 16:01:19 +0200 Subject: [PATCH 464/862] Update airvisual to use CoordinatorEntity (#39417) --- .../components/airvisual/__init__.py | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 4e748766425..563e24bf8fd 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -20,8 +20,11 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( CONF_CITY, @@ -351,20 +354,15 @@ async def async_update_options(hass, config_entry): await coordinator.async_request_refresh() -class AirVisualEntity(Entity): +class AirVisualEntity(CoordinatorEntity): """Define a generic AirVisual entity.""" def __init__(self, coordinator): """Initialize.""" + super().__init__(coordinator) self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._icon = None self._unit = None - self.coordinator = coordinator - - @property - def available(self): - """Return if entity is available.""" - return self.coordinator.last_update_success @property def device_state_attributes(self): @@ -376,11 +374,6 @@ class AirVisualEntity(Entity): """Return the icon.""" return self._icon - @property - def should_poll(self) -> bool: - """Disable polling.""" - return False - @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" @@ -399,13 +392,6 @@ class AirVisualEntity(Entity): self.update_from_latest_data() - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() - @callback def update_from_latest_data(self): """Update the entity from the latest data.""" From 29c1f873eb6047218f9437dbe682a983e9b3060b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 16:02:12 +0200 Subject: [PATCH 465/862] Update brother to use CoordinatorEntity (#39418) --- homeassistant/components/brother/sensor.py | 26 +++------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 607a5989abc..abc0447e718 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging from homeassistant.const import DEVICE_CLASS_TIMESTAMP -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow from .const import ( @@ -60,15 +60,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, False) -class BrotherPrinterSensor(Entity): +class BrotherPrinterSensor(CoordinatorEntity): """Define an Brother Printer sensor.""" def __init__(self, coordinator, kind, device_info): """Initialize.""" + super().__init__(coordinator) self._name = f"{coordinator.data[ATTR_MODEL]} {SENSOR_TYPES[kind][ATTR_LABEL]}" self._unique_id = f"{coordinator.data[ATTR_SERIAL].lower()}_{kind}" self._device_info = device_info - self.coordinator = coordinator self.kind = kind self._attrs = {} @@ -134,16 +134,6 @@ class BrotherPrinterSensor(Entity): """Return the unit the value is expressed in.""" return SENSOR_TYPES[self.kind][ATTR_UNIT] - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - - @property - def should_poll(self): - """Return the polling requirement of the entity.""" - return False - @property def device_info(self): """Return the device info.""" @@ -153,13 +143,3 @@ class BrotherPrinterSensor(Entity): def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" return True - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update Brother entity.""" - await self.coordinator.async_request_refresh() From 152b6c2d1a396daff78e1e3d66635260d45d7552 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 16:06:38 +0200 Subject: [PATCH 466/862] Update accuweather to use CoordinatorEntity (#39408) --- .../components/accuweather/sensor.py | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 9ab44a318a9..4f61322b2c6 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -5,7 +5,7 @@ from homeassistant.const import ( CONF_NAME, DEVICE_CLASS_TEMPERATURE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_FORECAST, @@ -48,14 +48,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, False) -class AccuWeatherSensor(Entity): +class AccuWeatherSensor(CoordinatorEntity): """Define an AccuWeather entity.""" def __init__(self, name, kind, coordinator, forecast_day=None): """Initialize.""" + super().__init__(coordinator) self._name = name self.kind = kind - self.coordinator = coordinator self._device_class = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial" @@ -85,16 +85,6 @@ class AccuWeatherSensor(Entity): "entry_type": "service", } - @property - def should_poll(self): - """Return the polling requirement of the entity.""" - return False - - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - @property def state(self): """Return the state.""" @@ -173,13 +163,3 @@ class AccuWeatherSensor(Entity): def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" return bool(self.kind not in OPTIONAL_SENSORS) - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update AccuWeather entity.""" - await self.coordinator.async_request_refresh() From 16a0bd7ff32c344ae4c22a2748f1f5a6fde1a30a Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Aug 2020 09:07:36 -0500 Subject: [PATCH 467/862] Update ipp to use CoordinatorEntity (#39412) * update ipp to use CoordinatorEntity * Update homeassistant/components/ipp/__init__.py Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> * Solve isort and black Co-authored-by: Paulus Schoutsen Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- homeassistant/components/ipp/__init__.py | 31 ++++++------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 1f9a616dc4f..9f522b086fc 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -18,8 +18,11 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( ATTR_IDENTIFIERS, @@ -124,7 +127,7 @@ class IPPDataUpdateCoordinator(DataUpdateCoordinator[IPPPrinter]): raise UpdateFailed(f"Invalid response from API: {error}") from error -class IPPEntity(Entity): +class IPPEntity(CoordinatorEntity): """Defines a base IPP entity.""" def __init__( @@ -138,12 +141,12 @@ class IPPEntity(Entity): enabled_default: bool = True, ) -> None: """Initialize the IPP entity.""" + super().__init__(coordinator) self._device_id = device_id self._enabled_default = enabled_default self._entry_id = entry_id self._icon = icon self._name = name - self.coordinator = coordinator @property def name(self) -> str: @@ -155,31 +158,11 @@ class IPPEntity(Entity): """Return the mdi icon of the entity.""" return self._icon - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.coordinator.last_update_success - @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self._enabled_default - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False - - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Update an IPP entity.""" - await self.coordinator.async_request_refresh() - @property def device_info(self) -> Dict[str, Any]: """Return device information about this IPP device.""" From 186c13f1ab3486f79f596531a5be6ac227d8c997 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 16:17:59 +0200 Subject: [PATCH 468/862] Update eafm to use CoordinatorEntity (#39420) --- homeassistant/components/eafm/sensor.py | 29 +++++-------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index ae4968bcf31..746f6f34abc 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -7,8 +7,10 @@ import async_timeout from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DOMAIN @@ -75,14 +77,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await coordinator.async_refresh() -class Measurement(Entity): +class Measurement(CoordinatorEntity): """A gauge at a flood monitoring station.""" attribution = "This uses Environment Agency flood and river level data from the real-time data API" def __init__(self, coordinator, key): """Initialise the gauge with a data instance and station.""" - self.coordinator = coordinator + super().__init__(coordinator) self.key = key @property @@ -110,11 +112,6 @@ class Measurement(Entity): """Return the name of the gauge.""" return f"{self.station_name} {self.parameter_name} {self.qualifier}" - @property - def should_poll(self) -> bool: - """Stations are polled as a group - the entity shouldn't poll by itself.""" - return False - @property def unique_id(self): """Return the unique id of the gauge.""" @@ -150,12 +147,6 @@ class Measurement(Entity): return True - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - @property def unit_of_measurement(self): """Return units for the sensor.""" @@ -173,11 +164,3 @@ class Measurement(Entity): def state(self): """Return the current sensor value.""" return self.coordinator.data["measures"][self.key]["latestReading"]["value"] - - async def async_update(self): - """ - Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() From baf5750425cc6c771674809ee184d24fc1db192a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 16:37:03 +0200 Subject: [PATCH 469/862] Update gios to use CoordinatorEntity (#39421) --- homeassistant/components/gios/air_quality.py | 25 +++----------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py index 3f6c85bdb97..8fcd84a4622 100644 --- a/homeassistant/components/gios/air_quality.py +++ b/homeassistant/components/gios/air_quality.py @@ -9,6 +9,7 @@ from homeassistant.components.air_quality import ( AirQualityEntity, ) from homeassistant.const import CONF_NAME +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTR_STATION, DEFAULT_NAME, DOMAIN, ICONS_MAP, MANUFACTURER @@ -45,12 +46,12 @@ def round_state(func): return _decorator -class GiosAirQuality(AirQualityEntity): +class GiosAirQuality(CoordinatorEntity, AirQualityEntity): """Define an GIOS sensor.""" def __init__(self, coordinator, name): """Initialize.""" - self.coordinator = coordinator + super().__init__(coordinator) self._name = name self._attrs = {} @@ -127,16 +128,6 @@ class GiosAirQuality(AirQualityEntity): "entry_type": "service", } - @property - def should_poll(self): - """Return the polling requirement of the entity.""" - return False - - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - @property def device_state_attributes(self): """Return the state attributes.""" @@ -150,16 +141,6 @@ class GiosAirQuality(AirQualityEntity): self._attrs[ATTR_STATION] = self.coordinator.gios.station_name return self._attrs - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update GIOS entity.""" - await self.coordinator.async_request_refresh() - def _get_sensor_value(self, sensor): """Return value of specified sensor.""" if sensor in self.coordinator.data: From a1d48ad1ecbf983b0649953985180a04f93fb879 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 16:38:15 +0200 Subject: [PATCH 470/862] Update juicenet to use CoordinatorEntity (#39424) --- homeassistant/components/juicenet/entity.py | 26 +++------------------ 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index fe81f242cd0..759979c5f11 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -1,44 +1,24 @@ """Adapter to wrap the pyjuicenet api for home assistant.""" -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -class JuiceNetDevice(Entity): +class JuiceNetDevice(CoordinatorEntity): """Represent a base JuiceNet device.""" def __init__(self, device, sensor_type, coordinator): """Initialise the sensor.""" + super().__init__(coordinator) self.device = device self.type = sensor_type - self.coordinator = coordinator @property def name(self): """Return the name of the device.""" return self.device.name - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - - async def async_update(self): - """Update the entity.""" - await self.coordinator.async_request_refresh() - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - @property def unique_id(self): """Return a unique ID.""" From 4a21b1f589ef65231783109cef2e80a705b06166 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 09:43:30 -0500 Subject: [PATCH 471/862] Update smart_meter_texas to use CoordinatorEntity (#39426) --- .../components/smart_meter_texas/sensor.py | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/smart_meter_texas/sensor.py b/homeassistant/components/smart_meter_texas/sensor.py index a655a805065..c08301d7021 100644 --- a/homeassistant/components/smart_meter_texas/sensor.py +++ b/homeassistant/components/smart_meter_texas/sensor.py @@ -5,9 +5,11 @@ from smart_meter_texas import Meter from homeassistant.const import CONF_ADDRESS, ENERGY_KILO_WATT_HOUR from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import ( DATA_COORDINATOR, @@ -31,13 +33,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class SmartMeterTexasSensor(RestoreEntity, Entity): +class SmartMeterTexasSensor(CoordinatorEntity, RestoreEntity): """Representation of an Smart Meter Texas sensor.""" def __init__(self, meter: Meter, coordinator: DataUpdateCoordinator): """Initialize the sensor.""" + super().__init__(coordinator) self.meter = meter - self.coordinator = coordinator self._state = None self._available = False @@ -76,18 +78,6 @@ class SmartMeterTexasSensor(RestoreEntity, Entity): } return attributes - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() - @callback def _state_update(self): """Call when the coordinator has an update.""" From 70c241b51bfca4059419ea03f544ffc86003a86e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 16:51:50 +0200 Subject: [PATCH 472/862] Fix pylint erros for sharkiq (#39428) --- homeassistant/components/sharkiq/config_flow.py | 8 ++++---- homeassistant/components/sharkiq/update_coordinator.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index b2b85d6cf36..34328efc26d 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -30,10 +30,10 @@ async def validate_input(hass: core.HomeAssistant, data): with async_timeout.timeout(10): LOGGER.debug("Initialize connection to Ayla networks API") await ayla_api.async_sign_in() - except (asyncio.TimeoutError, aiohttp.ClientError): - raise CannotConnect - except SharkIqAuthError: - raise InvalidAuth + except (asyncio.TimeoutError, aiohttp.ClientError) as errors: + raise CannotConnect from errors + except SharkIqAuthError as error: + raise InvalidAuth from error # Return info that you want to store in the config entry. return {"title": data[CONF_USERNAME]} diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index c498307ac9d..dff3681bba7 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -94,9 +94,9 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): ) ) - raise UpdateFailed(err) + raise UpdateFailed(err) from err except Exception as err: # pylint: disable=broad-except LOGGER.exception("Unexpected error updating SharkIQ", exc_info=err) - raise UpdateFailed(err) + raise UpdateFailed(err) from err return True From 0a389a4651128e3415674aa5b900e58c1e5a77c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 09:52:48 -0500 Subject: [PATCH 473/862] Update sharkiq to use CoordinatorEntity (#39427) * Update sharkiq to use CoordinatorEntity Fix pylint * revert --- homeassistant/components/sharkiq/vacuum.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index 5be95fdb516..f4549e9eb35 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -23,6 +23,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, SHARK from .update_coordinator import SharkIqUpdateCoordinator @@ -65,23 +66,18 @@ ATTR_RECHARGE_RESUME = "recharge_and_resume" ATTR_RSSI = "rssi" -class SharkVacuumEntity(StateVacuumEntity): +class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): """Shark IQ vacuum entity.""" def __init__(self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator): """Create a new SharkVacuumEntity.""" + super().__init__(coordinator) if sharkiq.serial_number not in coordinator.shark_vacs: raise RuntimeError( f"Shark IQ robot {sharkiq.serial_number} is not known to the coordinator" ) - self.coordinator = coordinator self.sharkiq = sharkiq - @property - def should_poll(self): - """Don't poll this entity. Polling is done via the coordinator.""" - return False - def clean_spot(self, **kwargs): """Clean a spot. Not yet implemented.""" raise NotImplementedError() @@ -189,16 +185,6 @@ class SharkVacuumEntity(StateVacuumEntity): """Get the current battery level.""" return self.sharkiq.get_property_value(Properties.BATTERY_CAPACITY) - async def async_update(self): - """Update the known properties asynchronously.""" - await self.coordinator.async_request_refresh() - - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - async def async_return_to_base(self, **kwargs): """Have the device return to base.""" await self.sharkiq.async_set_operating_mode(OperatingModes.RETURN) From e98480110432750f1c8bedb80affe16ed8c38266 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 17:02:55 +0200 Subject: [PATCH 474/862] Update iammeter to use CoordinatorEntity (#39423) * Update iammeter to use CoordinatorEntity * Remove async_will_remove_from_hass and async_added_to_hass --- homeassistant/components/iammeter/sensor.py | 33 +++++---------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index e2bf879e326..f885c2c49d3 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -13,8 +13,11 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import debounce import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) _LOGGER = logging.getLogger(__name__) @@ -71,12 +74,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class IamMeter(Entity): +class IamMeter(CoordinatorEntity): """Class for a sensor.""" def __init__(self, coordinator, uid, sensor_name, unit, dev_name): """Initialize an iammeter sensor.""" - self.coordinator = coordinator + super().__init__(coordinator) self.uid = uid self.sensor_name = sensor_name self.unit = unit @@ -106,25 +109,3 @@ class IamMeter(Entity): def unit_of_measurement(self): """Return the unit of measurement.""" return self.unit - - @property - def should_poll(self): - """Poll needed.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self.coordinator.last_update_success - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.coordinator.async_add_listener(self.async_write_ha_state) - - async def async_will_remove_from_hass(self): - """When entity will be removed from hass.""" - self.coordinator.async_remove_listener(self.async_write_ha_state) - - async def async_update(self): - """Update the entity.""" - await self.coordinator.async_request_refresh() From 1e97ca14f1c6fb7f9bf0a46b06cb1c8ab96f4bbc Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 17:38:26 +0200 Subject: [PATCH 475/862] Update accuweather to fully use CoordinatorEntity (#39431) --- .../components/accuweather/weather.py | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 47d1fef7b14..3c0dcfedf43 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -13,6 +13,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utc_from_timestamp from .const import ( @@ -37,13 +38,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([AccuWeatherEntity(name, coordinator)], False) -class AccuWeatherEntity(WeatherEntity): +class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): """Define an AccuWeather entity.""" def __init__(self, name, coordinator): """Initialize.""" + super().__init__(coordinator) self._name = name - self.coordinator = coordinator self._attrs = {} self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial" @@ -72,16 +73,6 @@ class AccuWeatherEntity(WeatherEntity): "entry_type": "service", } - @property - def should_poll(self): - """Return the polling requirement of the entity.""" - return False - - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - @property def condition(self): """Return the current condition.""" @@ -169,16 +160,6 @@ class AccuWeatherEntity(WeatherEntity): ] return forecast - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update AccuWeather entity.""" - await self.coordinator.async_request_refresh() - @staticmethod def _calc_precipitation(day: dict) -> float: """Return sum of the precipitation.""" From e701001c5f054c2e20ff4d3de9459b51b053ace9 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 17:47:45 +0200 Subject: [PATCH 476/862] Update poolsense to use CoordinatorEntity (#39435) --- .../components/poolsense/__init__.py | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index c3577982dc0..315816f4e1c 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -12,8 +12,11 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import DOMAIN @@ -78,13 +81,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -class PoolSenseEntity(Entity): +class PoolSenseEntity(CoordinatorEntity): """Implements a common class elements representing the PoolSense component.""" def __init__(self, coordinator, email, info_type): """Initialize poolsense sensor.""" + super().__init__(coordinator) self._unique_id = f"{email}-{info_type}" - self.coordinator = coordinator self.info_type = info_type @property @@ -92,26 +95,6 @@ class PoolSenseEntity(Entity): """Return a unique id.""" return self._unique_id - @property - def available(self): - """Return if sensor is available.""" - return self.coordinator.last_update_success - - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False - - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Request an update of the coordinator for entity.""" - await self.coordinator.async_request_refresh() - class PoolSenseDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold PoolSense data.""" From 22b8d43bf576fda03cae6b0ce63235fb88b42856 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 17:48:15 +0200 Subject: [PATCH 477/862] Update tesla to use CoordinatorEntity (#39436) --- homeassistant/components/tesla/__init__.py | 29 ++++++---------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 9b8962ae196..a04c5975881 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -21,8 +21,11 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from homeassistant.util import slugify from .config_flow import ( @@ -247,13 +250,13 @@ class TeslaDataUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(f"Error communicating with API: {err}") from err -class TeslaDevice(Entity): +class TeslaDevice(CoordinatorEntity): """Representation of a Tesla device.""" def __init__(self, tesla_device, coordinator): """Initialise the Tesla device.""" + super().__init__(coordinator) self.tesla_device = tesla_device - self.coordinator = coordinator self._name = self.tesla_device.name self._unique_id = slugify(self.tesla_device.uniq_name) self._attributes = self.tesla_device.attrs.copy() @@ -276,16 +279,6 @@ class TeslaDevice(Entity): return ICONS.get(self.tesla_device.type) - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self.coordinator.last_update_success - @property def device_state_attributes(self): """Return the state attributes of the device.""" @@ -310,14 +303,6 @@ class TeslaDevice(Entity): """Register state update callback.""" self.async_on_remove(self.coordinator.async_add_listener(self.refresh)) - async def async_will_remove_from_hass(self): - """Prepare for unload.""" - - async def async_update(self): - """Update the state of the device.""" - _LOGGER.debug("Updating state for: %s", self.name) - await self.coordinator.async_request_refresh() - @callback def refresh(self) -> None: """Refresh the state of the device. From 77fe20608454ec50b7490c4f4eeb5c105c05a3bf Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Sun, 30 Aug 2020 18:01:04 +0200 Subject: [PATCH 478/862] Update stookalert to use DEVICE_CLASS_SAFETY constant (#39438) asper discussion here https://github.com/home-assistant/core/pull/39310#issuecomment-681968732 --- homeassistant/components/stookalert/binary_sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index df0c1db369c..aa792d10653 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -5,7 +5,11 @@ import logging import stookalert import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SAFETY, + PLATFORM_SCHEMA, + BinarySensorEntity, +) from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.helpers import config_validation as cv @@ -13,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=60) CONF_PROVINCE = "province" -DEFAULT_DEVICE_CLASS = "safety" +DEFAULT_DEVICE_CLASS = DEVICE_CLASS_SAFETY DEFAULT_NAME = "Stookalert" ATTRIBUTION = "Data provided by rivm.nl" PROVINCES = [ From ce1ef733660a3b24c0b958a24a7d8d8866288f84 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 18:05:55 +0200 Subject: [PATCH 479/862] Update WLED to use CoordinatorEntity (#39442) --- homeassistant/components/wled/__init__.py | 31 +++++------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 5da55d0e3bd..d8aacd59881 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -14,9 +14,12 @@ from homeassistant.const import ATTR_NAME, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( ATTR_IDENTIFIERS, @@ -139,7 +142,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): raise UpdateFailed(f"Invalid response from API: {error}") from error -class WLEDEntity(Entity): +class WLEDEntity(CoordinatorEntity): """Defines a base WLED entity.""" def __init__( @@ -152,12 +155,12 @@ class WLEDEntity(Entity): enabled_default: bool = True, ) -> None: """Initialize the WLED entity.""" + super().__init__(coordinator) self._enabled_default = enabled_default self._entry_id = entry_id self._icon = icon self._name = name self._unsub_dispatcher = None - self.coordinator = coordinator @property def name(self) -> str: @@ -169,31 +172,11 @@ class WLEDEntity(Entity): """Return the mdi icon of the entity.""" return self._icon - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.coordinator.last_update_success - @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self._enabled_default - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False - - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Update WLED entity.""" - await self.coordinator.async_request_refresh() - class WLEDDeviceEntity(WLEDEntity): """Defines a WLED device entity.""" From 7bed868d603192bf6629c16d8a452f8b1dd56f4b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 18:10:28 +0200 Subject: [PATCH 480/862] Update wolflink to use CoordinatorEntity (#39444) --- homeassistant/components/wolflink/sensor.py | 27 +++------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/wolflink/sensor.py b/homeassistant/components/wolflink/sensor.py index a9deced9e91..97f48e27988 100644 --- a/homeassistant/components/wolflink/sensor.py +++ b/homeassistant/components/wolflink/sensor.py @@ -25,7 +25,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_HOURS, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity _LOGGER = logging.getLogger(__name__) @@ -55,12 +55,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class WolfLinkSensor(Entity): +class WolfLinkSensor(CoordinatorEntity): """Base class for all Wolf entities.""" def __init__(self, coordinator, wolf_object: Parameter, device_id): """Initialize.""" - self.coordinator = coordinator + super().__init__(coordinator) self.wolf_object = wolf_object self.device_id = device_id @@ -88,27 +88,6 @@ class WolfLinkSensor(Entity): """Return a unique_id for this entity.""" return f"{self.device_id}:{self.wolf_object.parameter_id}" - @property - def available(self): - """Return True if entity is available.""" - return self.coordinator.last_update_success - - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update the sensor.""" - await self.coordinator.async_request_refresh() - _LOGGER.debug("Updating %s", self.coordinator.data[self.wolf_object.value_id]) - class WolfLinkHours(WolfLinkSensor): """Class for hour based entities.""" From 42818c150f490d4b2fa7bd67e99e55e908f98185 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 18:10:45 +0200 Subject: [PATCH 481/862] Update pi_hole to use CoordinatorEntity (#39433) --- homeassistant/components/pi_hole/__init__.py | 31 +++++--------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index a1aa0f819f8..c9b7937da73 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -18,8 +18,11 @@ from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( CONF_LOCATION, @@ -150,22 +153,16 @@ def _async_platforms(entry): return platforms -class PiHoleEntity(Entity): +class PiHoleEntity(CoordinatorEntity): """Representation of a Pi-hole entity.""" def __init__(self, api, coordinator, name, server_unique_id): """Initialize a Pi-hole entity.""" + super().__init__(coordinator) self.api = api - self.coordinator = coordinator self._name = name self._server_unique_id = server_unique_id - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - @property def icon(self): """Icon to use in the frontend, if any.""" @@ -179,17 +176,3 @@ class PiHoleEntity(Entity): "name": self._name, "manufacturer": "Pi-hole", } - - @property - def available(self): - """Could the device be accessed during the last update call.""" - return self.coordinator.last_update_success - - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - async def async_update(self): - """Get the latest data from the Pi-hole API.""" - await self.coordinator.async_request_refresh() From 225743a6200d21210fc7b685fdd72e50d21da5ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 11:17:41 -0500 Subject: [PATCH 482/862] Update upnp to use CoordinatorEntity (#39434) --- homeassistant/components/upnp/sensor.py | 36 ++++++++----------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 5a34405e0c1..80a5ce7021c 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -5,9 +5,11 @@ from typing import Any, Mapping from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import ( BYTES_RECEIVED, @@ -119,7 +121,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class UpnpSensor(Entity): +class UpnpSensor(CoordinatorEntity): """Base class for UPnP/IGD sensors.""" def __init__( @@ -130,17 +132,12 @@ class UpnpSensor(Entity): update_multiplier: int = 2, ) -> None: """Initialize the base sensor.""" - self._coordinator = coordinator + super().__init__(coordinator) self._device = device self._sensor_type = sensor_type self._update_counter_max = update_multiplier self._update_counter = 0 - @property - def should_poll(self) -> bool: - """Inform we should not be polled.""" - return False - @property def icon(self) -> str: """Icon to use in the frontend, if any.""" @@ -151,8 +148,8 @@ class UpnpSensor(Entity): """Return if entity is available.""" device_value_key = self._sensor_type["device_value_key"] return ( - self._coordinator.last_update_success - and device_value_key in self._coordinator.data + self.coordinator.last_update_success + and device_value_key in self.coordinator.data ) @property @@ -180,17 +177,6 @@ class UpnpSensor(Entity): "model": self._device.model_name, } - async def async_update(self): - """Request an update.""" - await self._coordinator.async_request_refresh() - - async def async_added_to_hass(self) -> None: - """Subscribe to sensors events.""" - remove_from_coordinator = self._coordinator.async_add_listener( - self.async_write_ha_state - ) - self.async_on_remove(remove_from_coordinator) - class RawUpnpSensor(UpnpSensor): """Representation of a UPnP/IGD sensor.""" @@ -199,7 +185,7 @@ class RawUpnpSensor(UpnpSensor): def state(self) -> str: """Return the state of the device.""" device_value_key = self._sensor_type["device_value_key"] - value = self._coordinator.data[device_value_key] + value = self.coordinator.data[device_value_key] if value is None: return None return format(value, "d") @@ -238,10 +224,10 @@ class DerivedUpnpSensor(UpnpSensor): """Return the state of the device.""" # Can't calculate any derivative if we have only one value. device_value_key = self._sensor_type["device_value_key"] - current_value = self._coordinator.data[device_value_key] + current_value = self.coordinator.data[device_value_key] if current_value is None: return None - current_timestamp = self._coordinator.data[TIMESTAMP] + current_timestamp = self.coordinator.data[TIMESTAMP] if self._last_value is None or self._has_overflowed(current_value): self._last_value = current_value self._last_timestamp = current_timestamp From cceaa088cb18b23be8b0a148cb4bc5da8d960d42 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 18:26:37 +0200 Subject: [PATCH 483/862] Update toon to use CoordinatorEntity (#39441) --- homeassistant/components/toon/models.py | 26 +++---------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index 441b718c40a..4047ac0d744 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict, Optional -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import ToonDataUpdateCoordinator @@ -10,7 +10,7 @@ from .coordinator import ToonDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -class ToonEntity(Entity): +class ToonEntity(CoordinatorEntity): """Defines a base Toon entity.""" def __init__( @@ -22,11 +22,11 @@ class ToonEntity(Entity): enabled_default: bool = True, ) -> None: """Initialize the Toon entity.""" + super().__init__(coordinator) self._enabled_default = enabled_default self._icon = icon self._name = name self._state = None - self.coordinator = coordinator @property def name(self) -> str: @@ -38,31 +38,11 @@ class ToonEntity(Entity): """Return the mdi icon of the entity.""" return self._icon - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.coordinator.last_update_success - @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self._enabled_default - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False - - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Update Toon entity.""" - await self.coordinator.async_request_refresh() - class ToonDisplayDeviceEntity(ToonEntity): """Defines a Toon display device entity.""" From d9c9adbc91993a63c084e389dbfa1fa913d65ce8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 11:27:06 -0500 Subject: [PATCH 484/862] Update tankerkoenig to use CoordinatorEntity (#39440) --- .../components/tankerkoenig/sensor.py | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index b6c80de69b7..b0dd3368ad3 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -3,8 +3,11 @@ import logging from homeassistant.const import ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import DOMAIN, NAME @@ -71,15 +74,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) -class FuelPriceSensor(Entity): +class FuelPriceSensor(CoordinatorEntity): """Contains prices for fuel in a given station.""" def __init__(self, fuel_type, station, coordinator, name, show_on_map): """Initialize the sensor.""" + super().__init__(coordinator) self._station = station self._station_id = station["id"] self._fuel_type = fuel_type - self._coordinator = coordinator self._name = name self._latitude = station["lat"] self._longitude = station["lng"] @@ -105,16 +108,11 @@ class FuelPriceSensor(Entity): """Return unit of measurement.""" return "€" - @property - def should_poll(self): - """No need to poll. Coordinator notifies of updates.""" - return False - @property def state(self): """Return the state of the device.""" # key Fuel_type is not available when the fuel station is closed, use "get" instead of "[]" to avoid exceptions - return self._coordinator.data[self._station_id].get(self._fuel_type) + return self.coordinator.data[self._station_id].get(self._fuel_type) @property def unique_id(self) -> str: @@ -124,7 +122,7 @@ class FuelPriceSensor(Entity): @property def device_state_attributes(self): """Return the attributes of the device.""" - data = self._coordinator.data[self._station_id] + data = self.coordinator.data[self._station_id] attrs = { ATTR_ATTRIBUTION: ATTRIBUTION, @@ -144,20 +142,3 @@ class FuelPriceSensor(Entity): if data is not None and "status" in data: attrs[ATTR_IS_OPEN] = data["status"] == "open" return attrs - - @property - def available(self): - """Return if entity is available.""" - return self._coordinator.last_update_success - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self._coordinator.async_add_listener(self.async_write_ha_state) - - async def async_will_remove_from_hass(self): - """When entity will be removed from hass.""" - self._coordinator.async_remove_listener(self.async_write_ha_state) - - async def async_update(self): - """Update the entity.""" - await self._coordinator.async_request_refresh() From d3b845c01a167097ac8da49fcca880ebb4468eee Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 18:32:32 +0200 Subject: [PATCH 485/862] Update tile to use CoordinatorEntity (#39439) --- homeassistant/components/tile/__init__.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 58295c98bef..ad4a8886873 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -8,8 +8,11 @@ from pytile.errors import SessionExpiredError, TileError from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import DATA_COORDINATOR, DOMAIN, LOGGER @@ -85,15 +88,15 @@ async def async_unload_entry(hass, config_entry): return unload_ok -class TileEntity(Entity): +class TileEntity(CoordinatorEntity): """Define a generic Tile entity.""" def __init__(self, coordinator): """Initialize.""" + super().__init__(coordinator) self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._name = None self._unique_id = None - self.coordinator = coordinator @property def device_state_attributes(self): @@ -110,11 +113,6 @@ class TileEntity(Entity): """Return the name.""" return self._name - @property - def should_poll(self): - """Disable polling.""" - return False - @property def unique_id(self): """Return the unique ID of the entity.""" @@ -137,10 +135,3 @@ class TileEntity(Entity): self.async_on_remove(self.coordinator.async_add_listener(update)) self._update_from_latest_data() - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() From 17734d54abc0eb3ef85a72a24ce63cbcbce3400a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 18:32:56 +0200 Subject: [PATCH 486/862] Update hue to use CoordinatorEntity (#39446) --- homeassistant/components/hue/light.py | 28 +++++++-------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 9d055ad6066..20066bd1be2 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -29,7 +29,11 @@ from homeassistant.components.light import ( from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from homeassistant.util import color from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY @@ -194,13 +198,13 @@ def hass_to_hue_brightness(value): return max(1, round((value / 255) * 254)) -class HueLight(LightEntity): +class HueLight(CoordinatorEntity, LightEntity): """Representation of a Hue light.""" def __init__(self, coordinator, bridge, is_group, light, supported_features): """Initialize the light.""" + super().__init__(coordinator) self.light = light - self.coordinator = coordinator self.bridge = bridge self.is_group = is_group self._supported_features = supported_features @@ -236,11 +240,6 @@ class HueLight(LightEntity): """Return the unique ID of this Hue light.""" return self.light.uniqueid - @property - def should_poll(self): - """No polling required.""" - return False - @property def device_id(self): """Return the ID of this Hue light.""" @@ -371,12 +370,6 @@ class HueLight(LightEntity): "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), } - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {"on": True} @@ -462,13 +455,6 @@ class HueLight(LightEntity): await self.coordinator.async_request_refresh() - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() - @property def device_state_attributes(self): """Return the device state attributes.""" From 8b893173fd8069f241babbbdc36ce7451853ca73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 12:10:22 -0500 Subject: [PATCH 487/862] Prevent CoordinatorEntity from requesting updates on disabled entities (#39452) --- homeassistant/helpers/update_coordinator.py | 5 +++++ tests/helpers/test_update_coordinator.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index b17ddcd3dd6..44e10243598 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -221,4 +221,9 @@ class CoordinatorEntity(entity.Entity): Only used by the generic entity update service. """ + + # Ignore manual update requests if the entity is disabled + if not self.enabled: + return + await self.coordinator.async_request_refresh() diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 73360a3053b..ee3c4af6daf 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -244,3 +244,9 @@ async def test_coordinator_entity(crd): await entity.async_added_to_hass() assert mock_async_on_remove.called + + # Verify we do not update if the entity is disabled + crd.last_update_success = False + with patch("homeassistant.helpers.entity.Entity.enabled", False): + await entity.async_update() + assert entity.available is False From bd7682a69462ab2a5d7fdc0d1fd42d7ce8cb3142 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Aug 2020 12:42:53 -0500 Subject: [PATCH 488/862] Update roku state faster after actions (#39453) * update roku state faster after actions * Update media_player.py * Update remote.py --- homeassistant/components/roku/media_player.py | 11 +++++++++++ homeassistant/components/roku/remote.py | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 30cb4ec7a3e..d4a5f656c82 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -192,44 +192,52 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): async def async_turn_on(self) -> None: """Turn on the Roku.""" await self.coordinator.roku.remote("poweron") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_turn_off(self) -> None: """Turn off the Roku.""" await self.coordinator.roku.remote("poweroff") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_media_pause(self) -> None: """Send pause command.""" if self.state not in (STATE_STANDBY, STATE_PAUSED): await self.coordinator.roku.remote("play") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_media_play(self) -> None: """Send play command.""" if self.state not in (STATE_STANDBY, STATE_PLAYING): await self.coordinator.roku.remote("play") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_media_play_pause(self) -> None: """Send play/pause command.""" if self.state != STATE_STANDBY: await self.coordinator.roku.remote("play") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_media_previous_track(self) -> None: """Send previous track command.""" await self.coordinator.roku.remote("reverse") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_media_next_track(self) -> None: """Send next track command.""" await self.coordinator.roku.remote("forward") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_mute_volume(self, mute) -> None: """Mute the volume.""" await self.coordinator.roku.remote("volume_mute") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_volume_up(self) -> None: @@ -253,6 +261,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return await self.coordinator.roku.tune(media_id) + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_select_source(self, source: str) -> None: @@ -271,3 +280,5 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if appl is not None: await self.coordinator.roku.launch(appl.app_id) + + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 5b893b6a0f8..3fcd2ee1a34 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -47,11 +47,13 @@ class RokuRemote(RokuEntity, RemoteEntity): async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self.coordinator.roku.remote("poweron") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self.coordinator.roku.remote("poweroff") + await self.coordinator.async_request_refresh() @roku_exception_handler async def async_send_command(self, command: List, **kwargs) -> None: @@ -61,3 +63,5 @@ class RokuRemote(RokuEntity, RemoteEntity): for _ in range(num_repeats): for single_command in command: await self.coordinator.roku.remote(single_command) + + await self.coordinator.async_request_refresh() From 953626b2d4e7bf23ceaec99e4f9d962964bf3204 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 19:52:53 +0200 Subject: [PATCH 489/862] Update meteo_france to use CoordinatorEntity (#39432) * Update meteo_france to use CoordinatorEntity * Update homeassistant/components/meteo_france/sensor.py * Update homeassistant/components/meteo_france/weather.py Co-authored-by: J. Nick Koston --- .../components/meteo_france/sensor.py | 36 ++++--------------- .../components/meteo_france/weather.py | 32 ++++------------- 2 files changed, 13 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index de767217cbf..1e4c9b1215f 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -8,9 +8,11 @@ from meteofrance.helpers import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from homeassistant.util import dt as dt_util from .const import ( @@ -70,13 +72,13 @@ async def async_setup_entry( ) -class MeteoFranceSensor(Entity): +class MeteoFranceSensor(CoordinatorEntity): """Representation of a Meteo-France sensor.""" def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator): """Initialize the Meteo-France sensor.""" + super().__init__(coordinator) self._type = sensor_type - self.coordinator = coordinator city_name = self.coordinator.data.position["name"] self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}" self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}" @@ -142,29 +144,6 @@ class MeteoFranceSensor(Entity): """Return the state attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - @property - def available(self): - """Return if state is available.""" - return self.coordinator.last_update_success - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - - async def async_update(self): - """Only used by the generic entity update service.""" - if not self.enabled: - return - - await self.coordinator.async_request_refresh() - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - class MeteoFranceRainSensor(MeteoFranceSensor): """Representation of a Meteo-France rain sensor.""" @@ -203,8 +182,7 @@ class MeteoFranceAlertSensor(MeteoFranceSensor): # pylint: disable=super-init-not-called def __init__(self, sensor_type: str, coordinator: DataUpdateCoordinator): """Initialize the Meteo-France sensor.""" - self._type = sensor_type - self.coordinator = coordinator + super().__init__(sensor_type, coordinator) dept_code = self.coordinator.data.domain_id self._name = f"{dept_code} {SENSOR_TYPES[self._type][ENTITY_NAME]}" self._unique_id = self._name diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 30f3a350299..ffb468574b8 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -15,7 +15,10 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODE, TEMP_CELSIUS from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from homeassistant.util import dt as dt_util from .const import ( @@ -60,12 +63,12 @@ async def async_setup_entry( ) -class MeteoFranceWeather(WeatherEntity): +class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" def __init__(self, coordinator: DataUpdateCoordinator, mode: str): """Initialise the platform with a data instance and station name.""" - self.coordinator = coordinator + super().__init__(coordinator) self._city_name = self.coordinator.data.position["name"] self._mode = mode self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}" @@ -171,26 +174,3 @@ class MeteoFranceWeather(WeatherEntity): def attribution(self): """Return the attribution.""" return ATTRIBUTION - - @property - def available(self): - """Return if state is available.""" - return self.coordinator.last_update_success - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - - async def async_update(self): - """Only used by the generic entity update service.""" - if not self.enabled: - return - - await self.coordinator.async_request_refresh() - - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) From 773860ca5c090f0e4ae3a5b326c1af2c82324c0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:02:27 -0500 Subject: [PATCH 490/862] Update plugwise to use CoordinatorEntity (#39457) --- homeassistant/components/plugwise/__init__.py | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 7e43a68b9e8..3cfff3a8521 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -14,8 +14,11 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import DOMAIN @@ -131,13 +134,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -class SmileGateway(Entity): +class SmileGateway(CoordinatorEntity): """Represent Smile Gateway.""" def __init__(self, api, coordinator, name, dev_id): """Initialise the gateway.""" + super().__init__(coordinator) + self._api = api - self._coordinator = coordinator self._name = name self._dev_id = dev_id @@ -151,16 +155,6 @@ class SmileGateway(Entity): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - - @property - def available(self): - """Return True if entity is available.""" - return self._coordinator.last_update_success - @property def name(self): """Return the name of the entity, if any.""" @@ -188,14 +182,10 @@ class SmileGateway(Entity): """Subscribe to updates.""" self._async_process_data() self.async_on_remove( - self._coordinator.async_add_listener(self._async_process_data) + self.coordinator.async_add_listener(self._async_process_data) ) @callback def _async_process_data(self): """Interpret and process API data.""" raise NotImplementedError - - async def async_update(self): - """Update the entity.""" - await self._coordinator.async_request_refresh() From 692ed8c639fb82c6dc374b8785d6b55ceb1d7adc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:02:56 -0500 Subject: [PATCH 491/862] Update notion to use CoordinatorEntity (#39460) --- homeassistant/components/notion/__init__.py | 36 +++++++++---------- .../components/notion/binary_sensor.py | 4 +-- homeassistant/components/notion/sensor.py | 4 +-- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 72b6ac610db..1e7d12df38c 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -16,8 +16,11 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import DATA_COORDINATOR, DOMAIN @@ -166,7 +169,7 @@ async def async_register_new_bridge( ) -class NotionEntity(Entity): +class NotionEntity(CoordinatorEntity): """Define a base Notion entity.""" def __init__( @@ -180,9 +183,9 @@ class NotionEntity(Entity): device_class: str, ): """Initialize the entity.""" + super().__init__(coordinator) self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._bridge_id = bridge_id - self._coordinator = coordinator self._device_class = device_class self._name = name self._sensor_id = sensor_id @@ -194,8 +197,8 @@ class NotionEntity(Entity): def available(self) -> bool: """Return True if entity is available.""" return ( - self._coordinator.last_update_success - and self._task_id in self._coordinator.data["tasks"] + self.coordinator.last_update_success + and self._task_id in self.coordinator.data["tasks"] ) @property @@ -211,8 +214,8 @@ class NotionEntity(Entity): @property def device_info(self) -> dict: """Return device registry information for this entity.""" - bridge = self._coordinator.data["bridges"].get(self._bridge_id, {}) - sensor = self._coordinator.data["sensors"][self._sensor_id] + bridge = self.coordinator.data["bridges"].get(self._bridge_id, {}) + sensor = self.coordinator.data["sensors"][self._sensor_id] return { "identifiers": {(DOMAIN, sensor["hardware_id"])}, @@ -226,18 +229,13 @@ class NotionEntity(Entity): @property def name(self) -> str: """Return the name of the entity.""" - sensor = self._coordinator.data["sensors"][self._sensor_id] + sensor = self.coordinator.data["sensors"][self._sensor_id] return f'{sensor["name"]}: {self._name}' - @property - def should_poll(self) -> bool: - """Disable entity polling.""" - return False - @property def unique_id(self) -> str: """Return a unique, unchanging string that represents this entity.""" - task = self._coordinator.data["tasks"][self._task_id] + task = self.coordinator.data["tasks"][self._task_id] return f'{self._sensor_id}_{task["task_type"]}' async def _async_update_bridge_id(self) -> None: @@ -245,21 +243,21 @@ class NotionEntity(Entity): Sensors can move to other bridges based on signal strength, etc. """ - sensor = self._coordinator.data["sensors"][self._sensor_id] + sensor = self.coordinator.data["sensors"][self._sensor_id] # If the sensor's bridge ID is the same as what we had before or if it points # to a bridge that doesn't exist (which can happen due to a Notion API bug), # return immediately: if ( self._bridge_id == sensor["bridge"]["id"] - or sensor["bridge"]["id"] not in self._coordinator.data["bridges"] + or sensor["bridge"]["id"] not in self.coordinator.data["bridges"] ): return self._bridge_id = sensor["bridge"]["id"] device_registry = await dr.async_get_registry(self.hass) - bridge = self._coordinator.data["bridges"][self._bridge_id] + bridge = self.coordinator.data["bridges"][self._bridge_id] bridge_device = device_registry.async_get_device( {DOMAIN: bridge["hardware_id"]}, set() ) @@ -285,6 +283,6 @@ class NotionEntity(Entity): self._async_update_from_latest_data() self.async_write_ha_state() - self.async_on_remove(self._coordinator.async_add_listener(update)) + self.async_on_remove(self.coordinator.async_add_listener(update)) self._async_update_from_latest_data() diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 0be4a5336cb..e798b538565 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -73,12 +73,12 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - self._state = self._coordinator.data["tasks"][self._task_id]["status"]["value"] + self._state = self.coordinator.data["tasks"][self._task_id]["status"]["value"] @property def is_on(self) -> bool: """Return whether the sensor is on or off.""" - task = self._coordinator.data["tasks"][self._task_id] + task = self.coordinator.data["tasks"][self._task_id] if task["task_type"] == SENSOR_BATTERY: return self._state != "battery_good" diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index f810245b7e2..091dcd324dc 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -79,13 +79,13 @@ class NotionSensor(NotionEntity): @callback def _async_update_from_latest_data(self) -> None: """Fetch new state data for the sensor.""" - task = self._coordinator.data["tasks"][self._task_id] + task = self.coordinator.data["tasks"][self._task_id] if task["task_type"] == SENSOR_TEMPERATURE: self._state = round(float(task["status"]["value"]), 1) else: _LOGGER.error( "Unknown task type: %s: %s", - self._coordinator.data["sensors"][self._sensor_id], + self.coordinator.data["sensors"][self._sensor_id], task["task_type"], ) From e5b360cd08f5c5c4a494bb1873cb819782ab836d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 30 Aug 2020 20:04:01 +0200 Subject: [PATCH 492/862] Update coronavirus to use CoordinatorEntity (#39449) * Update coronavirus to use CoordinatorEntity * Remove should_poll from coronavirus --- homeassistant/components/coronavirus/sensor.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index d24c33a7752..7f0e0c230e6 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -1,6 +1,6 @@ """Sensor platform for the Corona virus.""" from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import get_coordinator from .const import ATTRIBUTION, OPTION_WORLDWIDE @@ -23,7 +23,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class CoronavirusSensor(Entity): +class CoronavirusSensor(CoordinatorEntity): """Sensor representing corona virus data.""" name = None @@ -31,12 +31,12 @@ class CoronavirusSensor(Entity): def __init__(self, coordinator, country, info_type): """Initialize coronavirus sensor.""" + super().__init__(coordinator) if country == OPTION_WORLDWIDE: self.name = f"Worldwide Coronavirus {info_type}" else: self.name = f"{coordinator.data[country].country} Coronavirus {info_type}" self.unique_id = f"{country}-{info_type}" - self.coordinator = coordinator self.country = country self.info_type = info_type @@ -76,11 +76,3 @@ class CoronavirusSensor(Entity): def device_state_attributes(self): """Return device attributes.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.coordinator.async_add_listener(self.async_write_ha_state) - - async def async_will_remove_from_hass(self): - """When entity will be removed from hass.""" - self.coordinator.async_remove_listener(self.async_write_ha_state) From 2b78d5235d1d7261d03b6ed90f4eb84996e32a91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:04:30 -0500 Subject: [PATCH 493/862] Update risco to use CoordinatorEntity (#39456) --- .../components/risco/alarm_control_panel.py | 4 +-- .../components/risco/binary_sensor.py | 2 +- homeassistant/components/risco/entity.py | 29 +++---------------- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index e6548c2ffdc..705ec50db28 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -65,7 +65,7 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): """Init the partition.""" super().__init__(coordinator) self._partition_id = partition_id - self._partition = self._coordinator.data.partitions[self._partition_id] + self._partition = self.coordinator.data.partitions[self._partition_id] self._code = code self._code_arm_required = options[CONF_CODE_ARM_REQUIRED] self._code_disarm_required = options[CONF_CODE_DISARM_REQUIRED] @@ -76,7 +76,7 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): self._supported_states |= STATES_TO_SUPPORTED_FEATURES[state] def _get_data_from_coordinator(self): - self._partition = self._coordinator.data.partitions[self._partition_id] + self._partition = self.coordinator.data.partitions[self._partition_id] @property def device_info(self): diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index f3c35071111..757a30252c2 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -39,7 +39,7 @@ class RiscoBinarySensor(BinarySensorEntity, RiscoEntity): self._zone = zone def _get_data_from_coordinator(self): - self._zone = self._coordinator.data.zones[self._zone_id] + self._zone = self.coordinator.data.zones[self._zone_id] @property def device_info(self): diff --git a/homeassistant/components/risco/entity.py b/homeassistant/components/risco/entity.py index 0c74cdf8264..17e27caf18b 100644 --- a/homeassistant/components/risco/entity.py +++ b/homeassistant/components/risco/entity.py @@ -1,24 +1,10 @@ """A risco entity base class.""" -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity -class RiscoEntity(Entity): +class RiscoEntity(CoordinatorEntity): """Risco entity base class.""" - def __init__(self, coordinator): - """Init the instance.""" - self._coordinator = coordinator - - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self._coordinator.last_update_success - def _get_data_from_coordinator(self): raise NotImplementedError @@ -29,17 +15,10 @@ class RiscoEntity(Entity): async def async_added_to_hass(self): """When entity is added to hass.""" self.async_on_remove( - self._coordinator.async_add_listener(self._refresh_from_coordinator) + self.coordinator.async_add_listener(self._refresh_from_coordinator) ) @property def _risco(self): """Return the Risco API object.""" - return self._coordinator.risco - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self._coordinator.async_request_refresh() + return self.coordinator.risco From 9f4d4862b4cc2cb7a7f8244e89488671c55828da Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 30 Aug 2020 20:13:47 +0200 Subject: [PATCH 494/862] Update xknx to 0.13.0 (#39407) --- homeassistant/components/knx/__init__.py | 84 ++++--------------- homeassistant/components/knx/binary_sensor.py | 15 +--- homeassistant/components/knx/climate.py | 16 +--- homeassistant/components/knx/const.py | 4 +- homeassistant/components/knx/cover.py | 15 +--- homeassistant/components/knx/factory.py | 30 +++---- homeassistant/components/knx/light.py | 15 +--- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/notify.py | 20 ++--- homeassistant/components/knx/scene.py | 16 +--- homeassistant/components/knx/sensor.py | 15 +--- homeassistant/components/knx/switch.py | 15 +--- requirements_all.txt | 2 +- 13 files changed, 71 insertions(+), 178 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 989efc9b376..cfa01d93d97 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change_event -from .const import DATA_KNX, DOMAIN, DeviceTypes +from .const import DATA_KNX, DOMAIN, SupportedPlatforms from .factory import create_knx_device from .schema import ( BinarySensorSchema, @@ -51,22 +51,11 @@ CONF_KNX_STATE_UPDATER = "state_updater" CONF_KNX_RATE_LIMIT = "rate_limit" CONF_KNX_EXPOSE = "expose" -CONF_KNX_LIGHT = "light" -CONF_KNX_COVER = "cover" -CONF_KNX_BINARY_SENSOR = "binary_sensor" -CONF_KNX_SCENE = "scene" -CONF_KNX_SENSOR = "sensor" -CONF_KNX_SWITCH = "switch" -CONF_KNX_NOTIFY = "notify" -CONF_KNX_CLIMATE = "climate" - SERVICE_KNX_SEND = "send" SERVICE_KNX_ATTR_ADDRESS = "address" SERVICE_KNX_ATTR_PAYLOAD = "payload" SERVICE_KNX_ATTR_TYPE = "type" -ATTR_DISCOVER_DEVICES = "devices" - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -89,28 +78,28 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_KNX_EXPOSE): vol.All( cv.ensure_list, [ExposeSchema.SCHEMA] ), - vol.Optional(CONF_KNX_COVER): vol.All( + vol.Optional(SupportedPlatforms.cover.value): vol.All( cv.ensure_list, [CoverSchema.SCHEMA] ), - vol.Optional(CONF_KNX_BINARY_SENSOR): vol.All( + vol.Optional(SupportedPlatforms.binary_sensor.value): vol.All( cv.ensure_list, [BinarySensorSchema.SCHEMA] ), - vol.Optional(CONF_KNX_LIGHT): vol.All( + vol.Optional(SupportedPlatforms.light.value): vol.All( cv.ensure_list, [LightSchema.SCHEMA] ), - vol.Optional(CONF_KNX_CLIMATE): vol.All( + vol.Optional(SupportedPlatforms.climate.value): vol.All( cv.ensure_list, [ClimateSchema.SCHEMA] ), - vol.Optional(CONF_KNX_NOTIFY): vol.All( + vol.Optional(SupportedPlatforms.notify.value): vol.All( cv.ensure_list, [NotifySchema.SCHEMA] ), - vol.Optional(CONF_KNX_SWITCH): vol.All( + vol.Optional(SupportedPlatforms.switch.value): vol.All( cv.ensure_list, [SwitchSchema.SCHEMA] ), - vol.Optional(CONF_KNX_SENSOR): vol.All( + vol.Optional(SupportedPlatforms.sensor.value): vol.All( cv.ensure_list, [SensorSchema.SCHEMA] ), - vol.Optional(CONF_KNX_SCENE): vol.All( + vol.Optional(SupportedPlatforms.scene.value): vol.All( cv.ensure_list, [SceneSchema.SCHEMA] ), } @@ -129,17 +118,6 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema( } ) -KNX_CONFIG_PLATFORM_MAPPING = { - CONF_KNX_COVER: DeviceTypes.cover, - CONF_KNX_SWITCH: DeviceTypes.switch, - CONF_KNX_LIGHT: DeviceTypes.light, - CONF_KNX_SENSOR: DeviceTypes.sensor, - CONF_KNX_NOTIFY: DeviceTypes.notify, - CONF_KNX_SCENE: DeviceTypes.scene, - CONF_KNX_BINARY_SENSOR: DeviceTypes.binary_sensor, - CONF_KNX_CLIMATE: DeviceTypes.climate, -} - async def async_setup(hass, config): """Set up the KNX component.""" @@ -153,30 +131,17 @@ async def async_setup(hass, config): f"Can't connect to KNX interface:
{ex}", title="KNX" ) - for platform_config, device_type in KNX_CONFIG_PLATFORM_MAPPING.items(): - if platform_config in config[DOMAIN]: - for device_config in config[DOMAIN][platform_config]: - hass.data[DATA_KNX].xknx.devices.add( - create_knx_device( - hass, device_type, hass.data[DATA_KNX].xknx, device_config - ) + for platform in SupportedPlatforms: + if platform.value in config[DOMAIN]: + for device_config in config[DOMAIN][platform.value]: + create_knx_device( + hass, platform, hass.data[DATA_KNX].xknx, device_config ) - for component, discovery_type in ( - ("switch", "Switch"), - ("climate", "Climate"), - ("cover", "Cover"), - ("light", "Light"), - ("sensor", "Sensor"), - ("binary_sensor", "BinarySensor"), - ("scene", "Scene"), - ("notify", "Notification"), - ): - found_devices = _get_devices(hass, discovery_type) + # We need to wait until all entities are loaded into the device list since they could also be created from other platforms + for platform in SupportedPlatforms: hass.async_create_task( - discovery.async_load_platform( - hass, component, DOMAIN, {ATTR_DISCOVER_DEVICES: found_devices}, config - ) + discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config) ) hass.services.async_register( @@ -189,19 +154,6 @@ async def async_setup(hass, config): return True -def _get_devices(hass, discovery_type): - """Get the KNX devices.""" - return list( - map( - lambda device: device.name, - filter( - lambda device: type(device).__name__ == discovery_type, - hass.data[DATA_KNX].xknx.devices, - ), - ) - ) - - class KNXModule: """Representation of KNX Object.""" @@ -375,7 +327,6 @@ class KNXExposeTime: self.device = DateTime( self.xknx, "Time", broadcast_type=broadcast_type, group_address=self.address ) - self.xknx.devices.add(self.device) class KNXExposeSensor: @@ -405,7 +356,6 @@ class KNXExposeSensor: group_address=self.address, value_type=self.type, ) - self.xknx.devices.add(self.device) async_track_state_change_event( self.hass, [self.entity_id], self._async_entity_changed ) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index a18889e122a..f3b7e881134 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -4,22 +4,15 @@ from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.core import callback -from . import ATTR_DISCOVER_DEVICES, DATA_KNX +from . import DATA_KNX async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up binary sensor(s) for KNX platform.""" - if discovery_info is not None: - async_add_entities_discovery(hass, discovery_info, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up binary sensors for KNX platform configured via xknx.yaml.""" entities = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXBinarySensor(device)) + for device in hass.data[DATA_KNX].xknx.devices: + if isinstance(device, XknxBinarySensor): + entities.append(KNXBinarySensor(device)) async_add_entities(entities) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index db0559e5158..b5aaeb67907 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -13,9 +13,8 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import callback -from . import ATTR_DISCOVER_DEVICES, DATA_KNX +from . import DATA_KNX from .const import OPERATION_MODES, PRESET_MODES OPERATION_MODES_INV = dict(reversed(item) for item in OPERATION_MODES.items()) @@ -24,17 +23,10 @@ PRESET_MODES_INV = dict(reversed(item) for item in PRESET_MODES.items()) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up climate(s) for KNX platform.""" - if discovery_info is not None: - async_add_entities_discovery(hass, discovery_info, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up climates for KNX platform configured within platform.""" entities = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXClimate(device)) + for device in hass.data[DATA_KNX].xknx.devices: + if isinstance(device, XknxClimate): + entities.append(KNXClimate(device)) async_add_entities(entities) diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index fefb0cd73c0..f259638457a 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -28,8 +28,8 @@ class ColorTempModes(Enum): relative = "DPT-5.001" -class DeviceTypes(Enum): - """KNX device types.""" +class SupportedPlatforms(Enum): + """Supported platforms.""" cover = "cover" light = "light" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 583f41c48ca..8c50bb2afe9 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -15,22 +15,15 @@ from homeassistant.components.cover import ( from homeassistant.core import callback from homeassistant.helpers.event import async_track_utc_time_change -from . import ATTR_DISCOVER_DEVICES, DATA_KNX +from . import DATA_KNX async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up cover(s) for KNX platform.""" - if discovery_info is not None: - async_add_entities_discovery(hass, discovery_info, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up covers for KNX platform configured via xknx.yaml.""" entities = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXCover(device)) + for device in hass.data[DATA_KNX].xknx.devices: + if isinstance(device, XknxCover): + entities.append(KNXCover(device)) async_add_entities(entities) diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 64245d61a08..a596875c27b 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType -from .const import DATA_KNX, DOMAIN, ColorTempModes, DeviceTypes +from .const import DOMAIN, ColorTempModes, SupportedPlatforms from .schema import ( BinarySensorSchema, ClimateSchema, @@ -32,31 +32,34 @@ from .schema import ( def create_knx_device( - hass: HomeAssistant, device_type: DeviceTypes, knx_module: XKNX, config: ConfigType + hass: HomeAssistant, + platform: SupportedPlatforms, + knx_module: XKNX, + config: ConfigType, ) -> XknxDevice: """Return the requested XKNX device.""" - if device_type is DeviceTypes.light: + if platform is SupportedPlatforms.light: return _create_light(knx_module, config) - if device_type is DeviceTypes.cover: + if platform is SupportedPlatforms.cover: return _create_cover(knx_module, config) - if device_type is DeviceTypes.climate: - return _create_climate(hass, knx_module, config) + if platform is SupportedPlatforms.climate: + return _create_climate(knx_module, config) - if device_type is DeviceTypes.switch: + if platform is SupportedPlatforms.switch: return _create_switch(knx_module, config) - if device_type is DeviceTypes.sensor: + if platform is SupportedPlatforms.sensor: return _create_sensor(knx_module, config) - if device_type is DeviceTypes.notify: + if platform is SupportedPlatforms.notify: return _create_notify(knx_module, config) - if device_type is DeviceTypes.scene: + if platform is SupportedPlatforms.scene: return _create_scene(knx_module, config) - if device_type is DeviceTypes.binary_sensor: + if platform is SupportedPlatforms.binary_sensor: return _create_binary_sensor(hass, knx_module, config) @@ -120,9 +123,7 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: ) -def _create_climate( - hass: HomeAssistant, knx_module: XKNX, config: ConfigType -) -> XknxClimate: +def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate: """Return a KNX Climate device to be used within XKNX.""" climate_mode = XknxClimateMode( knx_module, @@ -163,7 +164,6 @@ def _create_climate( ), operation_modes=config.get(ClimateSchema.CONF_OPERATION_MODES), ) - hass.data[DATA_KNX].xknx.devices.add(climate_mode) return XknxClimate( knx_module, diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 2fc53fa3b7c..6d8438df0f9 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( from homeassistant.core import callback import homeassistant.util.color as color_util -from . import ATTR_DISCOVER_DEVICES, DATA_KNX +from . import DATA_KNX DEFAULT_COLOR = (0.0, 0.0) DEFAULT_BRIGHTNESS = 255 @@ -24,17 +24,10 @@ DEFAULT_WHITE_VALUE = 255 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up lights for KNX platform.""" - if discovery_info is not None: - async_add_entities_discovery(hass, discovery_info, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up lights for KNX platform configured via xknx.yaml.""" entities = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXLight(device)) + for device in hass.data[DATA_KNX].xknx.devices: + if isinstance(device, XknxLight): + entities.append(KNXLight(device)) async_add_entities(entities) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 108f3a2062a..8986d85b8b6 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,6 +2,6 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.12.0"], + "requirements": ["xknx==0.13.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"] } diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index fcb5bd352d5..e47cfca2794 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,25 +1,19 @@ """Support for KNX/IP notification services.""" +from typing import List + from xknx.devices import Notification as XknxNotification from homeassistant.components.notify import BaseNotificationService -from homeassistant.core import callback -from . import ATTR_DISCOVER_DEVICES, DATA_KNX +from . import DATA_KNX async def async_get_service(hass, config, discovery_info=None): """Get the KNX notification service.""" - if discovery_info is not None: - async_get_service_discovery(hass, discovery_info) - - -@callback -def async_get_service_discovery(hass, discovery_info): - """Set up notifications for KNX platform configured via xknx.yaml.""" notification_devices = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - notification_devices.append(device) + for device in hass.data[DATA_KNX].xknx.devices: + if isinstance(device, XknxNotification): + notification_devices.append(device) return ( KNXNotificationService(notification_devices) if notification_devices else None ) @@ -28,7 +22,7 @@ def async_get_service_discovery(hass, discovery_info): class KNXNotificationService(BaseNotificationService): """Implement demo notification service.""" - def __init__(self, devices: XknxNotification): + def __init__(self, devices: List[XknxNotification]): """Initialize the service.""" self.devices = devices diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index dfa667dcd4f..b4df94a0fd4 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -4,24 +4,16 @@ from typing import Any from xknx.devices import Scene as XknxScene from homeassistant.components.scene import Scene -from homeassistant.core import callback -from . import ATTR_DISCOVER_DEVICES, DATA_KNX +from . import DATA_KNX async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the scenes for KNX platform.""" - if discovery_info is not None: - async_add_entities_discovery(hass, discovery_info, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up scenes for KNX platform configured via xknx.yaml.""" entities = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXScene(device)) + for device in hass.data[DATA_KNX].xknx.devices: + if isinstance(device, XknxScene): + entities.append(KNXScene(device)) async_add_entities(entities) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 1fd8950a3fb..d87119239cf 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -4,22 +4,15 @@ from xknx.devices import Sensor as XknxSensor from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from . import ATTR_DISCOVER_DEVICES, DATA_KNX +from . import DATA_KNX async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up sensor(s) for KNX platform.""" - if discovery_info is not None: - async_add_entities_discovery(hass, discovery_info, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up sensors for KNX platform configured via xknx.yaml.""" entities = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXSensor(device)) + for device in hass.data[DATA_KNX].xknx.devices: + if isinstance(device, XknxSensor): + entities.append(KNXSensor(device)) async_add_entities(entities) diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index a6e7e583b88..c378d1b0ca4 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -4,22 +4,15 @@ from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback -from . import ATTR_DISCOVER_DEVICES, DATA_KNX +from . import DATA_KNX async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up switch(es) for KNX platform.""" - if discovery_info is not None: - async_add_entities_discovery(hass, discovery_info, async_add_entities) - - -@callback -def async_add_entities_discovery(hass, discovery_info, async_add_entities): - """Set up switches for KNX platform configured via xknx.yaml.""" entities = [] - for device_name in discovery_info[ATTR_DISCOVER_DEVICES]: - device = hass.data[DATA_KNX].xknx.devices[device_name] - entities.append(KNXSwitch(device)) + for device in hass.data[DATA_KNX].xknx.devices: + if isinstance(device, XknxSwitch): + entities.append(KNXSwitch(device)) async_add_entities(entities) diff --git a/requirements_all.txt b/requirements_all.txt index 40670f1324c..d4a91fc0bfb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2254,7 +2254,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.12.0 +xknx==0.13.0 # homeassistant.components.bluesound # homeassistant.components.rest From 9d1045dd7a5f4e7b3983c10d78153f8deb6a0b9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:14:09 -0500 Subject: [PATCH 495/862] Update met to use CoordinatorEntity (#39462) --- homeassistant/components/met/weather.py | 36 ++++++++----------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index e2827367757..e532c193e71 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -17,6 +17,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure @@ -77,37 +78,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class MetWeather(WeatherEntity): +class MetWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met.no weather condition.""" def __init__(self, coordinator, config, is_metric, hourly): """Initialise the platform with a data instance and site.""" + super().__init__(coordinator) self._config = config - self._coordinator = coordinator self._is_metric = is_metric self._hourly = hourly self._name_appendix = "-hourly" if hourly else "" - async def async_added_to_hass(self): - """Start fetching data.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Only used by the generic entity update service.""" - await self._coordinator.async_request_refresh() - @property def track_home(self): """Return if we are tracking home.""" return self._config.get(CONF_TRACK_HOME, False) - @property - def should_poll(self): - """No polling needed.""" - return False - @property def unique_id(self): """Return unique ID.""" @@ -132,12 +118,12 @@ class MetWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - return self._coordinator.data.current_weather_data.get("condition") + return self.coordinator.data.current_weather_data.get("condition") @property def temperature(self): """Return the temperature.""" - return self._coordinator.data.current_weather_data.get("temperature") + return self.coordinator.data.current_weather_data.get("temperature") @property def temperature_unit(self): @@ -147,7 +133,7 @@ class MetWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - pressure_hpa = self._coordinator.data.current_weather_data.get("pressure") + pressure_hpa = self.coordinator.data.current_weather_data.get("pressure") if self._is_metric or pressure_hpa is None: return pressure_hpa @@ -156,12 +142,12 @@ class MetWeather(WeatherEntity): @property def humidity(self): """Return the humidity.""" - return self._coordinator.data.current_weather_data.get("humidity") + return self.coordinator.data.current_weather_data.get("humidity") @property def wind_speed(self): """Return the wind speed.""" - speed_m_s = self._coordinator.data.current_weather_data.get("wind_speed") + speed_m_s = self.coordinator.data.current_weather_data.get("wind_speed") if self._is_metric or speed_m_s is None: return speed_m_s @@ -172,7 +158,7 @@ class MetWeather(WeatherEntity): @property def wind_bearing(self): """Return the wind direction.""" - return self._coordinator.data.current_weather_data.get("wind_bearing") + return self.coordinator.data.current_weather_data.get("wind_bearing") @property def attribution(self): @@ -183,5 +169,5 @@ class MetWeather(WeatherEntity): def forecast(self): """Return the forecast array.""" if self._hourly: - return self._coordinator.data.hourly_forecast - return self._coordinator.data.daily_forecast + return self.coordinator.data.hourly_forecast + return self.coordinator.data.daily_forecast From 80910b4f9a7586036846f45191dcb0b0f23f07be Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 30 Aug 2020 20:17:27 +0200 Subject: [PATCH 496/862] Add bieniu as code owner for shelly (#39467) --- CODEOWNERS | 2 +- homeassistant/components/shelly/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index bfaf15a2ca1..e4a93391aba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -368,7 +368,7 @@ homeassistant/components/seven_segments/* @fabaff homeassistant/components/seventeentrack/* @bachya homeassistant/components/sharkiq/* @ajmarks homeassistant/components/shell_command/* @home-assistant/core -homeassistant/components/shelly/* @balloob +homeassistant/components/shelly/* @balloob @bieniu homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/sighthound/* @robmarkcole diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 4f4e740a83f..f15c4fb3f0d 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/shelly2", "requirements": ["aioshelly==0.2.1"], "zeroconf": ["_http._tcp.local."], - "codeowners": ["@balloob"] + "codeowners": ["@balloob", "@bieniu"] } From c4f5a05b27be0a63eeb16ac249ec90d25a9a2440 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:18:19 -0500 Subject: [PATCH 497/862] Update coolmaster to use CoordinatorEntity (#39465) --- .../components/coolmaster/climate.py | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 8666307d65c..77afd85395a 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -14,6 +14,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN @@ -54,44 +55,27 @@ async def async_setup_entry(hass, config_entry, async_add_devices): async_add_devices(all_devices) -class CoolmasterClimate(ClimateEntity): +class CoolmasterClimate(CoordinatorEntity, ClimateEntity): """Representation of a coolmaster climate device.""" def __init__(self, coordinator, unit_id, unit, supported_modes, info): """Initialize the climate device.""" - self._coordinator = coordinator + super().__init__(coordinator) self._unit_id = unit_id self._unit = unit self._hvac_modes = supported_modes self._info = info - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self._coordinator.last_update_success - def _refresh_from_coordinator(self): - self._unit = self._coordinator.data[self._unit_id] + self._unit = self.coordinator.data[self._unit_id] self.async_write_ha_state() async def async_added_to_hass(self): """When entity is added to hass.""" self.async_on_remove( - self._coordinator.async_add_listener(self._refresh_from_coordinator) + self.coordinator.async_add_listener(self._refresh_from_coordinator) ) - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self._coordinator.async_request_refresh() - @property def device_info(self): """Return device info for this device.""" From 1fbb158211ad67845e202e0d93663ce2fb39bc96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:20:01 -0500 Subject: [PATCH 498/862] Update dexcom to use CoordinatorEntity (#39464) --- homeassistant/components/dexcom/sensor.py | 62 ++++------------------- 1 file changed, 11 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py index ac85e63b598..afb3feeec4d 100644 --- a/homeassistant/components/dexcom/sensor.py +++ b/homeassistant/components/dexcom/sensor.py @@ -1,6 +1,6 @@ """Support for Dexcom sensors.""" from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import COORDINATOR, DOMAIN, GLUCOSE_TREND_ICON, GLUCOSE_VALUE_ICON, MG_DL @@ -16,17 +16,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(sensors, False) -class DexcomGlucoseValueSensor(Entity): +class DexcomGlucoseValueSensor(CoordinatorEntity): """Representation of a Dexcom glucose value sensor.""" def __init__(self, coordinator, username, unit_of_measurement): """Initialize the sensor.""" + super().__init__(coordinator) self._state = None self._unit_of_measurement = unit_of_measurement self._attribute_unit_of_measurement = ( "mg_dl" if unit_of_measurement == MG_DL else "mmol_l" ) - self._coordinator = coordinator self._name = f"{DOMAIN}_{username}_glucose_value" self._unique_id = f"{username}-value" @@ -48,43 +48,23 @@ class DexcomGlucoseValueSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self._coordinator.data: - return getattr(self._coordinator.data, self._attribute_unit_of_measurement) + if self.coordinator.data: + return getattr(self.coordinator.data, self._attribute_unit_of_measurement) return None - @property - def available(self): - """Return True if entity is available.""" - return self._coordinator.last_update_success - - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - @property def unique_id(self): """Device unique id.""" return self._unique_id - async def async_update(self): - """Get the latest state of the sensor.""" - await self._coordinator.async_request_refresh() - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) - - -class DexcomGlucoseTrendSensor(Entity): +class DexcomGlucoseTrendSensor(CoordinatorEntity): """Representation of a Dexcom glucose trend sensor.""" def __init__(self, coordinator, username): """Initialize the sensor.""" + super().__init__(coordinator) self._state = None - self._coordinator = coordinator self._name = f"{DOMAIN}_{username}_glucose_trend" self._unique_id = f"{username}-trend" @@ -96,38 +76,18 @@ class DexcomGlucoseTrendSensor(Entity): @property def icon(self): """Return the icon for the frontend.""" - if self._coordinator.data: - return GLUCOSE_TREND_ICON[self._coordinator.data.trend] + if self.coordinator.data: + return GLUCOSE_TREND_ICON[self.coordinator.data.trend] return GLUCOSE_TREND_ICON[0] @property def state(self): """Return the state of the sensor.""" - if self._coordinator.data: - return self._coordinator.data.trend_description + if self.coordinator.data: + return self.coordinator.data.trend_description return None - @property - def available(self): - """Return True if entity is available.""" - return self._coordinator.last_update_success - - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - @property def unique_id(self): """Device unique id.""" return self._unique_id - - async def async_update(self): - """Get the latest state of the sensor.""" - await self._coordinator.async_request_refresh() - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) From ab0b0dc51c62e4479c50e9925b9fa55ee3983acc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:20:45 -0500 Subject: [PATCH 499/862] Update ovo_energy to use CoordinatorEntity (#39459) --- .../components/ovo_energy/__init__.py | 27 +++++-------------- homeassistant/components/ovo_energy/sensor.py | 16 +++++------ 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index e98e81ba1c2..445ae733ec5 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -11,9 +11,11 @@ from ovoenergy.ovoenergy import OVOEnergy from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN @@ -86,7 +88,7 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool return True -class OVOEnergyEntity(Entity): +class OVOEnergyEntity(CoordinatorEntity): """Defines a base OVO Energy entity.""" def __init__( @@ -98,7 +100,7 @@ class OVOEnergyEntity(Entity): icon: str, ) -> None: """Initialize the OVO Energy entity.""" - self._coordinator = coordinator + super().__init__(coordinator) self._client = client self._key = key self._name = name @@ -123,22 +125,7 @@ class OVOEnergyEntity(Entity): @property def available(self) -> bool: """Return True if entity is available.""" - return self._coordinator.last_update_success and self._available - - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - async def async_update(self) -> None: - """Update OVO Energy entity.""" - await self._coordinator.async_request_refresh() - - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) + return self.coordinator.last_update_success and self._available class OVOEnergyDeviceEntity(OVOEnergyEntity): diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 8b0cddd3752..c43b7ec8514 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -100,7 +100,7 @@ class OVOEnergyLastElectricityReading(OVOEnergySensor): @property def state(self) -> str: """Return the state of the sensor.""" - usage: OVODailyUsage = self._coordinator.data + usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: return None return usage.electricity[-1].consumption @@ -108,7 +108,7 @@ class OVOEnergyLastElectricityReading(OVOEnergySensor): @property def device_state_attributes(self) -> object: """Return the attributes of the sensor.""" - usage: OVODailyUsage = self._coordinator.data + usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: return None return { @@ -135,7 +135,7 @@ class OVOEnergyLastGasReading(OVOEnergySensor): @property def state(self) -> str: """Return the state of the sensor.""" - usage: OVODailyUsage = self._coordinator.data + usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: return None return usage.gas[-1].consumption @@ -143,7 +143,7 @@ class OVOEnergyLastGasReading(OVOEnergySensor): @property def device_state_attributes(self) -> object: """Return the attributes of the sensor.""" - usage: OVODailyUsage = self._coordinator.data + usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: return None return { @@ -171,7 +171,7 @@ class OVOEnergyLastElectricityCost(OVOEnergySensor): @property def state(self) -> str: """Return the state of the sensor.""" - usage: OVODailyUsage = self._coordinator.data + usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: return None return usage.electricity[-1].cost.amount @@ -179,7 +179,7 @@ class OVOEnergyLastElectricityCost(OVOEnergySensor): @property def device_state_attributes(self) -> object: """Return the attributes of the sensor.""" - usage: OVODailyUsage = self._coordinator.data + usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.electricity: return None return { @@ -207,7 +207,7 @@ class OVOEnergyLastGasCost(OVOEnergySensor): @property def state(self) -> str: """Return the state of the sensor.""" - usage: OVODailyUsage = self._coordinator.data + usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: return None return usage.gas[-1].cost.amount @@ -215,7 +215,7 @@ class OVOEnergyLastGasCost(OVOEnergySensor): @property def device_state_attributes(self) -> object: """Return the attributes of the sensor.""" - usage: OVODailyUsage = self._coordinator.data + usage: OVODailyUsage = self.coordinator.data if usage is None or not usage.gas: return None return { From b57f33c41ac5b41f02733b86f4cb602ad0f3e5c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:27:46 -0500 Subject: [PATCH 500/862] Update control4 to use CoordinatorEntity (#39466) --- homeassistant/components/control4/__init__.py | 31 +++++-------------- homeassistant/components/control4/light.py | 8 ++--- 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 41b71162d6c..a45cdc4006a 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -18,8 +18,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, device_registry as dr, entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers import aiohttp_client, device_registry as dr +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import ( CONF_ACCOUNT, @@ -154,7 +157,7 @@ async def get_items_of_category(hass: HomeAssistant, entry: ConfigEntry, categor return return_list -class Control4Entity(entity.Entity): +class Control4Entity(CoordinatorEntity): """Base entity for Control4.""" def __init__( @@ -170,11 +173,11 @@ class Control4Entity(entity.Entity): device_id: int, ): """Initialize a Control4 entity.""" + super().__init__(coordinator) self.entry = entry self.entry_data = entry_data self._name = name self._idx = idx - self._coordinator = coordinator self._controller_unique_id = entry_data[CONF_CONTROLLER_UNIQUE_ID] self._device_name = device_name self._device_manufacturer = device_manufacturer @@ -202,23 +205,3 @@ class Control4Entity(entity.Entity): "model": self._device_model, "via_device": (DOMAIN, self._controller_unique_id), } - - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self._coordinator.last_update_success - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update the state of the device.""" - await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index 09e05791169..08ac23e40e2 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -175,13 +175,13 @@ class Control4Light(Control4Entity, LightEntity): @property def is_on(self): """Return whether this light is on or off.""" - return self._coordinator.data[self._idx]["value"] > 0 + return self.coordinator.data[self._idx]["value"] > 0 @property def brightness(self): """Return the brightness of this light between 0..255.""" if self._is_dimmer: - return round(self._coordinator.data[self._idx]["value"] * 2.55) + return round(self.coordinator.data[self._idx]["value"] * 2.55) return None @property @@ -213,7 +213,7 @@ class Control4Light(Control4Entity, LightEntity): delay_time = (transition_length / 1000) + 0.7 _LOGGER.debug("Delaying light update by %s seconds", delay_time) await asyncio.sleep(delay_time) - await self._coordinator.async_request_refresh() + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" @@ -232,4 +232,4 @@ class Control4Light(Control4Entity, LightEntity): delay_time = (transition_length / 1000) + 0.7 _LOGGER.debug("Delaying light update by %s seconds", delay_time) await asyncio.sleep(delay_time) - await self._coordinator.async_request_refresh() + await self.coordinator.async_request_refresh() From 9ec870c75048488bd931f3ccde54942881f22c18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:28:09 -0500 Subject: [PATCH 501/862] Update hunterdouglas_powerview to use CoordinatorEntity (#39463) --- .../components/hunterdouglas_powerview/cover.py | 5 ++--- .../hunterdouglas_powerview/entity.py | 17 +++-------------- .../hunterdouglas_powerview/sensor.py | 4 ++-- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 871413b9f5b..402451da26e 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -110,7 +110,6 @@ class PowerViewShade(ShadeEntity, CoverEntity): self._scheduled_transition_update = None self._room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") self._current_cover_position = MIN_POSITION - self._coordinator = coordinator @property def device_state_attributes(self): @@ -272,7 +271,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): """When entity is added to hass.""" self._async_update_current_cover_position() self.async_on_remove( - self._coordinator.async_add_listener(self._async_update_shade_from_group) + self.coordinator.async_add_listener(self._async_update_shade_from_group) ) @callback @@ -282,5 +281,5 @@ class PowerViewShade(ShadeEntity, CoverEntity): # If a transition in in progress # the data will be wrong return - self._async_process_new_shade_data(self._coordinator.data[self._shade.id]) + self._async_process_new_shade_data(self.coordinator.data[self._shade.id]) self.async_write_ha_state() diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index f89ca28023b..4ed68fc3557 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -3,7 +3,7 @@ from aiopvapi.resources.shade import ATTR_TYPE import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( DEVICE_FIRMWARE, @@ -20,31 +20,20 @@ from .const import ( ) -class HDEntity(Entity): +class HDEntity(CoordinatorEntity): """Base class for hunter douglas entities.""" def __init__(self, coordinator, device_info, unique_id): """Initialize the entity.""" - super().__init__() - self._coordinator = coordinator + super().__init__(coordinator) self._unique_id = unique_id self._device_info = device_info - @property - def available(self): - """Return True if entity is available.""" - return self._coordinator.last_update_success - @property def unique_id(self): """Return the unique id.""" return self._unique_id - @property - def should_poll(self): - """Return False, updates are controlled via coordinator.""" - return False - @property def device_info(self): """Return the device_info of the device.""" diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 794fdac3eac..78c6fde75f5 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -76,11 +76,11 @@ class PowerViewShadeBatterySensor(ShadeEntity): async def async_added_to_hass(self): """When entity is added to hass.""" self.async_on_remove( - self._coordinator.async_add_listener(self._async_update_shade_from_group) + self.coordinator.async_add_listener(self._async_update_shade_from_group) ) @callback def _async_update_shade_from_group(self): """Update with new data from the coordinator.""" - self._shade.raw_data = self._coordinator.data[self._shade.id] + self._shade.raw_data = self.coordinator.data[self._shade.id] self.async_write_ha_state() From 56ddbd87b291046e9700e883f79f67812bce046d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 13:58:00 -0500 Subject: [PATCH 502/862] Fix recorder test intermittently failing (#39468) --- tests/components/recorder/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 07bfcb0bf4f..4c25771398c 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -13,6 +13,7 @@ def wait_recording_done(hass): trigger_db_commit(hass) hass.block_till_done() hass.data[recorder.DATA_INSTANCE].block_till_done() + hass.block_till_done() def trigger_db_commit(hass): From 65a9e18b27ef9aeb13c1ebe79fe056fe2087bd08 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Aug 2020 13:58:25 -0500 Subject: [PATCH 503/862] Improve patching in marytts tests (#39458) * improve patching in marytts tests * Update test_tts.py * Update test_tts.py * Update test_tts.py * Update test_tts.py * Update test_tts.py * Update test_tts.py --- tests/components/marytts/test_tts.py | 51 +++++++++++----------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index da221a7effd..b5681460b75 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -2,7 +2,6 @@ import asyncio import os import shutil -from urllib.parse import urlencode from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, @@ -11,10 +10,9 @@ from homeassistant.components.media_player.const import ( ) import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.setup import setup_component -from tests.async_mock import Mock, patch +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, mock_service @@ -62,18 +60,15 @@ class TestTTSMaryTTSPlatform: """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - conn = Mock() - response = Mock() - conn.getresponse.return_value = response - response.status = 200 - response.read.return_value = b"audio" - config = {tts.DOMAIN: {"platform": "marytts"}} with assert_setup_component(1, tts.DOMAIN): setup_component(self.hass, tts.DOMAIN, config) - with patch("http.client.HTTPConnection", return_value=conn): + with patch( + "homeassistant.components.marytts.tts.MaryTTS.speak", + return_value=b"audio", + ) as mock_speak: self.hass.services.call( tts.DOMAIN, "marytts_say", @@ -86,18 +81,14 @@ class TestTTSMaryTTSPlatform: assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 - conn.request.assert_called_with("POST", "/process", urlencode(self.params)) + + mock_speak.assert_called_once() + mock_speak.assert_called_with("HomeAssistant", {}) def test_service_say_with_effect(self): """Test service call say with effects.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - conn = Mock() - response = Mock() - conn.getresponse.return_value = response - response.status = 200 - response.read.return_value = b"audio" - config = { tts.DOMAIN: {"platform": "marytts", "effect": {"Volume": "amount:2.0;"}} } @@ -105,7 +96,10 @@ class TestTTSMaryTTSPlatform: with assert_setup_component(1, tts.DOMAIN): setup_component(self.hass, tts.DOMAIN, config) - with patch("http.client.HTTPConnection", return_value=conn): + with patch( + "homeassistant.components.marytts.tts.MaryTTS.speak", + return_value=b"audio", + ) as mock_speak: self.hass.services.call( tts.DOMAIN, "marytts_say", @@ -119,28 +113,22 @@ class TestTTSMaryTTSPlatform: assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 - self.params.update( - {"effect_Volume_selected": "on", "effect_Volume_parameters": "amount:2.0;"} - ) - conn.request.assert_called_with("POST", "/process", urlencode(self.params)) + mock_speak.assert_called_once() + mock_speak.assert_called_with("HomeAssistant", {"Volume": "amount:2.0;"}) def test_service_say_http_error(self): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - conn = Mock() - response = Mock() - conn.getresponse.return_value = response - response.status = HTTP_INTERNAL_SERVER_ERROR - response.reason = "test" - response.readline.return_value = "content" - config = {tts.DOMAIN: {"platform": "marytts"}} with assert_setup_component(1, tts.DOMAIN): setup_component(self.hass, tts.DOMAIN, config) - with patch("http.client.HTTPConnection", return_value=conn): + with patch( + "homeassistant.components.marytts.tts.MaryTTS.speak", + side_effect=Exception(), + ) as mock_speak: self.hass.services.call( tts.DOMAIN, "marytts_say", @@ -152,4 +140,5 @@ class TestTTSMaryTTSPlatform: self.hass.block_till_done() assert len(calls) == 0 - conn.request.assert_called_with("POST", "/process", urlencode(self.params)) + + mock_speak.assert_called_once() From 863c7414bcde37b7884d871b20b469b9d68c7d6e Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Aug 2020 13:59:15 -0500 Subject: [PATCH 504/862] Implement code review for nzbget (#39425) * implement code review for nzbget * Update strings.json * Update sensor.py * Update config_flow.py * Update sensor.py * Update config_flow.py * Update config_flow.py * Update config_flow.py --- .../components/nzbget/config_flow.py | 35 ++++++++++--------- homeassistant/components/nzbget/sensor.py | 18 +++++++--- homeassistant/components/nzbget/strings.json | 3 +- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index 77524cc3079..dfed7a9bfee 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -78,26 +78,27 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - if user_input is None: - return self._show_setup_form() + errors = {} - if CONF_VERIFY_SSL not in user_input: - user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL + if user_input is not None: + if CONF_VERIFY_SSL not in user_input: + user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL - try: - await self.hass.async_add_executor_job( - validate_input, self.hass, user_input - ) - except NZBGetAPIException: - return self._show_setup_form({"base": "cannot_connect"}) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - return self.async_abort(reason="unknown") + try: + await self.hass.async_add_executor_job( + validate_input, self.hass, user_input + ) + except NZBGetAPIException: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + else: + return self.async_create_entry( + title=user_input[CONF_HOST], + data=user_input, + ) - return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) - - def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: - """Show the setup form to the user.""" data_schema = { vol.Required(CONF_HOST): str, vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index e8e7b619f2c..c9437195826 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,4 +1,5 @@ """Monitor the NZBGet API.""" +from datetime import timedelta import logging from typing import Callable, List, Optional @@ -7,10 +8,11 @@ from homeassistant.const import ( CONF_NAME, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND, - TIME_MINUTES, + DEVICE_CLASS_TIMESTAMP, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.dt import utcnow from . import NZBGetEntity from .const import DATA_COORDINATOR, DOMAIN @@ -32,7 +34,7 @@ SENSOR_TYPES = { "post_job_count": ["PostJobCount", "Post Processing Jobs", "Jobs"], "post_paused": ["PostPaused", "Post Processing Paused", None], "remaining_size": ["RemainingSizeMB", "Queue Size", DATA_MEGABYTES], - "uptime": ["UpTimeSec", "Uptime", TIME_MINUTES], + "uptime": ["UpTimeSec", "Uptime", None], } @@ -85,6 +87,14 @@ class NZBGetSensor(NZBGetEntity, Entity): name=f"{entry_name} {sensor_name}", ) + @property + def device_class(self): + """Return the device class.""" + if "UpTimeSec" in self._sensor_type: + return DEVICE_CLASS_TIMESTAMP + + return None + @property def unique_id(self) -> str: """Return the unique ID of the sensor.""" @@ -109,7 +119,7 @@ class NZBGetSensor(NZBGetEntity, Entity): return round(value / 2 ** 20, 2) if "UpTimeSec" in self._sensor_type and value > 0: - # Convert uptime from seconds to minutes - return round(value / 60, 2) + uptime = utcnow() - timedelta(seconds=value) + return uptime.replace(microsecond=0).isoformat() return value diff --git a/homeassistant/components/nzbget/strings.json b/homeassistant/components/nzbget/strings.json index 9bbcd66781e..5a0c31054a9 100644 --- a/homeassistant/components/nzbget/strings.json +++ b/homeassistant/components/nzbget/strings.json @@ -16,8 +16,7 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", From 56057a638feb2e0d63b264b3a13c36a87d62e51f Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Sun, 30 Aug 2020 20:04:36 +0100 Subject: [PATCH 505/862] Clarify when message come from FCM (#39455) --- homeassistant/components/mobile_app/notify.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index f3c79103111..62bb5fdf08d 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -144,6 +144,14 @@ class MobileAppNotificationService(BaseNotificationService): f"Internal server error, please try again later: {fallback_error}" ) message = result.get("message", fallback_message) + + if "message" in result: + if message[-1] not in [".", "?", "!"]: + message += "." + message += ( + " This message is generated externally to Home Assistant." + ) + if response.status == 429: _LOGGER.warning(message) log_rate_limits( From 705b253a9941651536858fa1d33f55f2de2b9a67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 14:05:22 -0500 Subject: [PATCH 506/862] Update awair to use CoordinatorEntity (#39469) --- homeassistant/components/awair/sensor.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 28802454aa2..421fa3d8a26 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -13,6 +13,7 @@ from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( API_DUST, @@ -82,7 +83,7 @@ async def async_setup_entry( async_add_entities(sensors) -class AwairSensor(Entity): +class AwairSensor(CoordinatorEntity): """Defines an Awair sensor entity.""" def __init__( @@ -92,14 +93,9 @@ class AwairSensor(Entity): coordinator: AwairDataUpdateCoordinator, ) -> None: """Set up an individual AwairSensor.""" + super().__init__(coordinator) self._kind = kind self._device = device - self._coordinator = coordinator - - @property - def should_poll(self) -> bool: - """Return the polling requirement of the entity.""" - return False @property def name(self) -> str: @@ -128,7 +124,7 @@ class AwairSensor(Entity): def available(self) -> bool: """Determine if the sensor is available based on API results.""" # If the last update was successful... - if self._coordinator.last_update_success and self._air_data: + if self.coordinator.last_update_success and self._air_data: # and the results included our sensor type... if self._kind in self._air_data.sensors: # then we are available. @@ -231,20 +227,10 @@ class AwairSensor(Entity): return info - async def async_added_to_hass(self) -> None: - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self) -> None: - """Update Awair entity.""" - await self._coordinator.async_request_refresh() - @property def _air_data(self) -> Optional[AwairResult]: """Return the latest data for our device, or None.""" - result: Optional[AwairResult] = self._coordinator.data.get(self._device.uuid) + result: Optional[AwairResult] = self.coordinator.data.get(self._device.uuid) if result: return result.air_data From 25f9560fb658fc81ae42aa3581c33b6428ddd32a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 14:19:48 -0500 Subject: [PATCH 507/862] Update schluter to use CoordinatorEntity (#39454) --- homeassistant/components/schluter/climate.py | 42 +++++++------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index e41c733e83a..c7f8dc1d23d 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -17,7 +17,11 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, CONF_SCAN_INTERVAL -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from . import DATA_SCHLUTER_API, DATA_SCHLUTER_SESSION, DOMAIN @@ -63,27 +67,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class SchluterThermostat(ClimateEntity): +class SchluterThermostat(CoordinatorEntity, ClimateEntity): """Representation of a Schluter thermostat.""" def __init__(self, coordinator, serial_number, api, session_id): """Initialize the thermostat.""" - self._coordinator = coordinator + super().__init__(coordinator) self._serial_number = serial_number self._api = api self._session_id = session_id self._support_flags = SUPPORT_TARGET_TEMPERATURE - @property - def available(self): - """Return if thermostat is available.""" - return self._coordinator.last_update_success - - @property - def should_poll(self): - """Return if platform should poll.""" - return False - @property def supported_features(self): """Return the list of supported features.""" @@ -97,7 +91,7 @@ class SchluterThermostat(ClimateEntity): @property def name(self): """Return the name of the thermostat.""" - return self._coordinator.data[self._serial_number].name + return self.coordinator.data[self._serial_number].name @property def temperature_unit(self): @@ -107,7 +101,7 @@ class SchluterThermostat(ClimateEntity): @property def current_temperature(self): """Return the current temperature.""" - return self._coordinator.data[self._serial_number].temperature + return self.coordinator.data[self._serial_number].temperature @property def hvac_mode(self): @@ -119,14 +113,14 @@ class SchluterThermostat(ClimateEntity): """Return current operation. Can only be heating or idle.""" return ( CURRENT_HVAC_HEAT - if self._coordinator.data[self._serial_number].is_heating + if self.coordinator.data[self._serial_number].is_heating else CURRENT_HVAC_IDLE ) @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._coordinator.data[self._serial_number].set_point_temp + return self.coordinator.data[self._serial_number].set_point_temp @property def hvac_modes(self): @@ -136,12 +130,12 @@ class SchluterThermostat(ClimateEntity): @property def min_temp(self): """Identify min_temp in Schluter API.""" - return self._coordinator.data[self._serial_number].min_temp + return self.coordinator.data[self._serial_number].min_temp @property def max_temp(self): """Identify max_temp in Schluter API.""" - return self._coordinator.data[self._serial_number].max_temp + return self.coordinator.data[self._serial_number].max_temp async def async_set_hvac_mode(self, hvac_mode): """Mode is always heating, so do nothing.""" @@ -150,7 +144,7 @@ class SchluterThermostat(ClimateEntity): """Set new target temperature.""" target_temp = None target_temp = kwargs.get(ATTR_TEMPERATURE) - serial_number = self._coordinator.data[self._serial_number].serial_number + serial_number = self.coordinator.data[self._serial_number].serial_number _LOGGER.debug("Setting thermostat temperature: %s", target_temp) try: @@ -158,11 +152,3 @@ class SchluterThermostat(ClimateEntity): self._api.set_temperature(self._session_id, serial_number, target_temp) except RequestException as ex: _LOGGER.error("An error occurred while setting temperature: %s", ex) - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self._coordinator.async_add_listener(self.async_write_ha_state) - - async def async_will_remove_from_hass(self): - """When entity will be removed from hass.""" - self._coordinator.async_remove_listener(self.async_write_ha_state) From 4bbc737954325e84e42572e8ce7e40116d1a271e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2020 14:34:16 -0500 Subject: [PATCH 508/862] Fix light device trigger test flapping (#39470) --- tests/components/light/test_device_trigger.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index dd4745ac513..d3c630cd0dc 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -95,6 +95,8 @@ async def test_if_fires_on_state_change(hass, calls): platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() + await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() @@ -180,6 +182,8 @@ async def test_if_fires_on_state_change_with_for(hass, calls): platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() + await hass.async_block_till_done() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() From e707b50658200b4caa104d9ecef0c8c6b6de0ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20=C5=9EEREMET?= Date: Sun, 30 Aug 2020 23:03:33 +0300 Subject: [PATCH 509/862] Add integration for ProgettiHWSW automation boards (#37922) * Opened a new fresh page to clean my mess. * Solved pylint warnings * Fixing pylint issue of defining attr outside init. * Excluded files from being tested by codecov. * Solved binary sensor error. * Fixed some stylisation errors. * Resolved input not updating problem. * Added port entry to test file. * Added tests for create_entry. * Added support for better state management. * Increased code coverage of config_flow.py & made some tweaks. * Increased coverage of config_flow.py by adding tests for unknown exceptions. * A small bugfix. * Stylised code as per Chris' suggestions. * Stylised code again. * Improved quality of test code. * Added step_id in config flow tests. --- .coveragerc | 3 + CODEOWNERS | 1 + .../components/progettihwsw/__init__.py | 64 +++++++ .../components/progettihwsw/binary_sensor.py | 95 ++++++++++ .../components/progettihwsw/config_flow.py | 102 +++++++++++ .../components/progettihwsw/const.py | 5 + .../components/progettihwsw/manifest.json | 12 ++ .../components/progettihwsw/strings.json | 40 +++++ .../components/progettihwsw/switch.py | 109 ++++++++++++ .../progettihwsw/translations/en.json | 40 +++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/progettihwsw/__init__.py | 1 + .../progettihwsw/test_config_flow.py | 168 ++++++++++++++++++ 15 files changed, 647 insertions(+) create mode 100644 homeassistant/components/progettihwsw/__init__.py create mode 100644 homeassistant/components/progettihwsw/binary_sensor.py create mode 100644 homeassistant/components/progettihwsw/config_flow.py create mode 100644 homeassistant/components/progettihwsw/const.py create mode 100644 homeassistant/components/progettihwsw/manifest.json create mode 100644 homeassistant/components/progettihwsw/strings.json create mode 100644 homeassistant/components/progettihwsw/switch.py create mode 100644 homeassistant/components/progettihwsw/translations/en.json create mode 100644 tests/components/progettihwsw/__init__.py create mode 100644 tests/components/progettihwsw/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index d7d8880fdf0..1a7d1f7d394 100644 --- a/.coveragerc +++ b/.coveragerc @@ -667,6 +667,9 @@ omit = homeassistant/components/poolsense/sensor.py homeassistant/components/poolsense/binary_sensor.py homeassistant/components/proliphix/climate.py + homeassistant/components/progettihwsw/__init__.py + homeassistant/components/progettihwsw/binary_sensor.py + homeassistant/components/progettihwsw/switch.py homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py homeassistant/components/proxmoxve/* diff --git a/CODEOWNERS b/CODEOWNERS index e4a93391aba..ecf7745e595 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -325,6 +325,7 @@ homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa homeassistant/components/point/* @fredrike homeassistant/components/poolsense/* @haemishkyd homeassistant/components/powerwall/* @bdraco @jrester +homeassistant/components/progettihwsw/* @ardaseremet homeassistant/components/prometheus/* @knyar homeassistant/components/proxmoxve/* @k4ds3 @jhollowe homeassistant/components/ps4/* @ktnrg45 diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py new file mode 100644 index 00000000000..02418c963d4 --- /dev/null +++ b/homeassistant/components/progettihwsw/__init__.py @@ -0,0 +1,64 @@ +"""Automation manager for boards manufactured by ProgettiHWSW Italy.""" +import asyncio + +from ProgettiHWSW.ProgettiHWSWAPI import ProgettiHWSWAPI +from ProgettiHWSW.input import Input +from ProgettiHWSW.relay import Relay + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS = ["switch", "binary_sensor"] + + +async def async_setup(hass, config): + """Set up the ProgettiHWSW Automation component.""" + hass.data[DOMAIN] = {} + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up ProgettiHWSW Automation from a config entry.""" + + hass.data[DOMAIN][entry.entry_id] = ProgettiHWSWAPI( + f'{entry.data["host"]}:{entry.data["port"]}' + ) + + # Check board validation again to load new values to API. + await hass.data[DOMAIN][entry.entry_id].check_board() + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +def setup_input(api: ProgettiHWSWAPI, input_number: int) -> Input: + """Initialize the input pin.""" + return api.get_input(input_number) + + +def setup_switch(api: ProgettiHWSWAPI, switch_number: int, mode: str) -> Relay: + """Initialize the output pin.""" + return api.get_relay(switch_number, mode) diff --git a/homeassistant/components/progettihwsw/binary_sensor.py b/homeassistant/components/progettihwsw/binary_sensor.py new file mode 100644 index 00000000000..1ad0d919f15 --- /dev/null +++ b/homeassistant/components/progettihwsw/binary_sensor.py @@ -0,0 +1,95 @@ +"""Control binary sensor instances.""" + +from datetime import timedelta +import logging + +from ProgettiHWSW.input import Input +import async_timeout + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import setup_input +from .const import DEFAULT_POLLING_INTERVAL_SEC, DOMAIN + +_LOGGER = logging.getLogger(DOMAIN) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set the progettihwsw platform up and create sensor instances (legacy).""" + + return True + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the binary sensors from a config entry.""" + board_api = hass.data[DOMAIN][config_entry.entry_id] + input_count = config_entry.data["input_count"] + binary_sensors = [] + + async def async_update_data(): + """Fetch data from API endpoint of board.""" + async with async_timeout.timeout(5): + return await board_api.get_inputs() + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="binary_sensor", + update_method=async_update_data, + update_interval=timedelta(seconds=DEFAULT_POLLING_INTERVAL_SEC), + ) + await coordinator.async_refresh() + + for i in range(1, int(input_count) + 1): + binary_sensors.append( + ProgettihwswBinarySensor( + hass, + coordinator, + config_entry, + f"Input #{i}", + setup_input(board_api, i), + ) + ) + + async_add_entities(binary_sensors) + + +class ProgettihwswBinarySensor(BinarySensorEntity): + """Represent a binary sensor.""" + + def __init__(self, hass, coordinator, config_entry, name, sensor: Input): + """Set initializing values.""" + self._name = name + self._sensor = sensor + self._coordinator = coordinator + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) + + @property + def name(self): + """Return the sensor name.""" + return self._name + + @property + def is_on(self): + """Get sensor state.""" + return self._coordinator.data[self._sensor.id] + + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @property + def available(self): + """Return if entity is available.""" + return self._coordinator.last_update_success + + async def async_update(self): + """Update the state of binary sensor.""" + await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py new file mode 100644 index 00000000000..9306b134dbb --- /dev/null +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -0,0 +1,102 @@ +"""Config flow for ProgettiHWSW Automation integration.""" + +from ProgettiHWSW.ProgettiHWSWAPI import ProgettiHWSWAPI +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions + +from .const import DOMAIN # pylint: disable=unused-import + +DATA_SCHEMA = vol.Schema( + {vol.Required("host"): str, vol.Required("port", default=80): int} +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user host input.""" + + api_instance = ProgettiHWSWAPI(f'{data["host"]}:{data["port"]}') + is_valid = await api_instance.check_board() + + if is_valid is False: + raise CannotConnect + + return { + "title": is_valid["title"], + "relay_count": is_valid["relay_count"], + "input_count": is_valid["input_count"], + "is_old": is_valid["is_old"], + } + + +async def validate_input_relay_modes(data): + """Validate the user input in relay modes form.""" + for mode in data.values(): + if mode not in ("bistable", "monostable"): + raise WrongInfo + + return True + + +class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for ProgettiHWSW Automation.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_relay_modes(self, user_input=None): + """Manage relay modes step.""" + errors = {} + if user_input is not None: + try: + await validate_input_relay_modes(user_input) + whole_data = user_input + whole_data.update(self.s1_in) + except WrongInfo: + errors["base"] = "wrong_info_relay_modes" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=whole_data["title"], data=whole_data + ) + + relay_modes_schema = {} + for i in range(1, int(self.s1_in["relay_count"]) + 1): + relay_modes_schema[ + vol.Required(f"relay_{str(i)}", default="bistable") + ] = str + + return self.async_show_form( + step_id="relay_modes", + data_schema=vol.Schema(relay_modes_schema), + errors=errors, + ) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + user_input.update(info) + self.s1_in = ( # pylint: disable=attribute-defined-outside-init + user_input + ) + return await self.async_step_relay_modes() + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot identify host.""" + + +class WrongInfo(exceptions.HomeAssistantError): + """Error to indicate we cannot validate relay modes input.""" diff --git a/homeassistant/components/progettihwsw/const.py b/homeassistant/components/progettihwsw/const.py new file mode 100644 index 00000000000..11d54d89c41 --- /dev/null +++ b/homeassistant/components/progettihwsw/const.py @@ -0,0 +1,5 @@ +"""Define constant variables for general usage.""" + +DOMAIN = "progettihwsw" + +DEFAULT_POLLING_INTERVAL_SEC = 5 diff --git a/homeassistant/components/progettihwsw/manifest.json b/homeassistant/components/progettihwsw/manifest.json new file mode 100644 index 00000000000..15987837fb5 --- /dev/null +++ b/homeassistant/components/progettihwsw/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "progettihwsw", + "name": "ProgettiHWSW Automation", + "documentation": "https://www.home-assistant.io/integrations/progettihwsw", + "codeowners": [ + "@ardaseremet" + ], + "requirements": [ + "progettihwsw==0.1.1" + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/strings.json b/homeassistant/components/progettihwsw/strings.json new file mode 100644 index 00000000000..2c25433fba9 --- /dev/null +++ b/homeassistant/components/progettihwsw/strings.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "wrong_info_relay_modes": "Relay mode selection must be monostable or bistable." + }, + "step": { + "user": { + "title": "Set up board", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + }, + "relay_modes": { + "title": "Set up relays", + "data": { + "relay_1": "Relay 1", + "relay_2": "Relay 2", + "relay_3": "Relay 3", + "relay_4": "Relay 4", + "relay_5": "Relay 5", + "relay_6": "Relay 6", + "relay_7": "Relay 7", + "relay_8": "Relay 8", + "relay_9": "Relay 9", + "relay_10": "Relay 10", + "relay_11": "Relay 11", + "relay_12": "Relay 12", + "relay_13": "Relay 13", + "relay_14": "Relay 14", + "relay_15": "Relay 15", + "relay_16": "Relay 16" + } + } + } + }, + "title": "ProgettiHWSW Automation" +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/switch.py b/homeassistant/components/progettihwsw/switch.py new file mode 100644 index 00000000000..b6480d15c7b --- /dev/null +++ b/homeassistant/components/progettihwsw/switch.py @@ -0,0 +1,109 @@ +"""Control switches.""" + +from datetime import timedelta +import logging + +from ProgettiHWSW.relay import Relay +import async_timeout + +from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import setup_switch +from .const import DEFAULT_POLLING_INTERVAL_SEC, DOMAIN + +_LOGGER = logging.getLogger(DOMAIN) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set the switch platform up (legacy).""" + return True + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the switches from a config entry.""" + board_api = hass.data[DOMAIN][config_entry.entry_id] + relay_count = config_entry.data["relay_count"] + switches = [] + + async def async_update_data(): + """Fetch data from API endpoint of board.""" + async with async_timeout.timeout(5): + return await board_api.get_switches() + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="switch", + update_method=async_update_data, + update_interval=timedelta(seconds=DEFAULT_POLLING_INTERVAL_SEC), + ) + await coordinator.async_refresh() + + for i in range(1, int(relay_count) + 1): + switches.append( + ProgettihwswSwitch( + hass, + coordinator, + config_entry, + f"Relay #{i}", + setup_switch(board_api, i, config_entry.data[f"relay_{str(i)}"]), + ) + ) + + async_add_entities(switches) + + +class ProgettihwswSwitch(SwitchEntity): + """Represent a switch entity.""" + + def __init__(self, hass, coordinator, config_entry, name, switch: Relay): + """Initialize the values.""" + self._switch = switch + self._name = name + self._coordinator = coordinator + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self._switch.control(True) + await self._coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self._switch.control(False) + await self._coordinator.async_request_refresh() + + async def async_toggle(self, **kwargs): + """Toggle the state of switch.""" + await self._switch.toggle() + await self._coordinator.async_request_refresh() + + @property + def name(self): + """Return the switch name.""" + return self._name + + @property + def is_on(self): + """Get switch state.""" + return self._coordinator.data[self._switch.id] + + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @property + def available(self): + """Return if entity is available.""" + return self._coordinator.last_update_success + + async def async_update(self): + """Update the state of switch.""" + await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/progettihwsw/translations/en.json b/homeassistant/components/progettihwsw/translations/en.json new file mode 100644 index 00000000000..254620bcf8b --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "cannot_connect": "Cannot connect to the board.", + "unknown": "Unknown error.", + "wrong_info_relay_modes": "Relay mode selection must be monostable or bistable." + }, + "step": { + "user": { + "title": "Set up board", + "data": { + "host": "Host", + "port": "Port Number" + } + }, + "relay_modes": { + "title": "Set up relays", + "data": { + "relay_1": "Relay 1", + "relay_2": "Relay 2", + "relay_3": "Relay 3", + "relay_4": "Relay 4", + "relay_5": "Relay 5", + "relay_6": "Relay 6", + "relay_7": "Relay 7", + "relay_8": "Relay 8", + "relay_9": "Relay 9", + "relay_10": "Relay 10", + "relay_11": "Relay 11", + "relay_12": "Relay 12", + "relay_13": "Relay 13", + "relay_14": "Relay 14", + "relay_15": "Relay 15", + "relay_16": "Relay 16" + } + } + } + }, + "title": "ProgettiHWSW Automation" + } \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bc5470188b3..62877b614b1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -140,6 +140,7 @@ FLOWS = [ "point", "poolsense", "powerwall", + "progettihwsw", "ps4", "pvpc_hourly_pricing", "rachio", diff --git a/requirements_all.txt b/requirements_all.txt index d4a91fc0bfb..3c17612b7bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1115,6 +1115,9 @@ praw==7.1.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 +# homeassistant.components.progettihwsw +progettihwsw==0.1.1 + # homeassistant.components.proliphix proliphix==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42240019185..21d610c2aba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -539,6 +539,9 @@ praw==7.1.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 +# homeassistant.components.progettihwsw +progettihwsw==0.1.1 + # homeassistant.components.prometheus prometheus_client==0.7.1 diff --git a/tests/components/progettihwsw/__init__.py b/tests/components/progettihwsw/__init__.py new file mode 100644 index 00000000000..5049834cc10 --- /dev/null +++ b/tests/components/progettihwsw/__init__.py @@ -0,0 +1 @@ +"""Tests for the ProgettiHWSW Automation integration.""" diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py new file mode 100644 index 00000000000..7da0ef82642 --- /dev/null +++ b/tests/components/progettihwsw/test_config_flow.py @@ -0,0 +1,168 @@ +"""Test the ProgettiHWSW Automation config flow.""" +from homeassistant import config_entries, setup +from homeassistant.components.progettihwsw.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +from tests.async_mock import patch + +mock_value_step_user = { + "title": "1R & 1IN Board", + "relay_count": 1, + "input_count": 1, + "is_old": False, +} + + +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"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + mock_value_step_rm = { + "relay_1": "bistable", # Mocking a single relay board instance. + } + + with patch( + "homeassistant.components.progettihwsw.config_flow.ProgettiHWSWAPI.check_board", + return_value=mock_value_step_user, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "", CONF_PORT: 80}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "relay_modes" + assert result2["errors"] == {} + + with patch( + "homeassistant.components.progettihwsw.async_setup", + return_value=True, + ), patch( + "homeassistant.components.progettihwsw.async_setup_entry", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + mock_value_step_rm, + ) + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["data"] + assert result3["data"]["title"] == "1R & 1IN Board" + assert result3["data"]["is_old"] is False + assert result3["data"]["relay_count"] == result3["data"]["input_count"] == 1 + + +async def test_form_wrong_info(hass): + """Test we handle wrong info exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.progettihwsw.config_flow.ProgettiHWSWAPI.check_board", + return_value=mock_value_step_user, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: "", CONF_PORT: 80} + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "relay_modes" + assert result2["errors"] == {} + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {"relay_1": ""} + ) + + assert result3["type"] == RESULT_TYPE_FORM + assert result3["step_id"] == "relay_modes" + assert result3["errors"] == {"base": "wrong_info_relay_modes"} + + +async def test_form_cannot_connect(hass): + """Test we handle unexisting board.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.progettihwsw.config_flow.ProgettiHWSWAPI.check_board", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "", CONF_PORT: 80}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_user_exception(hass): + """Test we handle unknown exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.progettihwsw.config_flow.validate_input", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "", CONF_PORT: 80}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_rm_exception(hass): + """Test we handle unknown exception on seconds step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.progettihwsw.config_flow.ProgettiHWSWAPI.check_board", + return_value=mock_value_step_user, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "", CONF_PORT: 80}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "relay_modes" + assert result2["errors"] == {} + + with patch( + "homeassistant.components.progettihwsw.config_flow.validate_input_relay_modes", + side_effect=Exception, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"relay_1": "bistable"}, + ) + + assert result3["type"] == RESULT_TYPE_FORM + assert result3["step_id"] == "relay_modes" + assert result3["errors"] == {"base": "unknown"} From 159f6750d70956f271574535cbd9feb8a69fcdac Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Aug 2020 15:53:32 -0500 Subject: [PATCH 510/862] Update progettihwsw to use CoordinatorEntity (#39477) * update progettihwsw to use CoordinatorEntity * Update switch.py * Update binary_sensor.py * Update binary_sensor.py * Update switch.py --- .../components/progettihwsw/binary_sensor.py | 31 ++++------------ .../components/progettihwsw/switch.py | 37 +++++-------------- 2 files changed, 17 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/progettihwsw/binary_sensor.py b/homeassistant/components/progettihwsw/binary_sensor.py index 1ad0d919f15..e6ab49e8e5c 100644 --- a/homeassistant/components/progettihwsw/binary_sensor.py +++ b/homeassistant/components/progettihwsw/binary_sensor.py @@ -7,7 +7,10 @@ from ProgettiHWSW.input import Input import async_timeout from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import setup_input from .const import DEFAULT_POLLING_INTERVAL_SEC, DOMAIN @@ -55,20 +58,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(binary_sensors) -class ProgettihwswBinarySensor(BinarySensorEntity): +class ProgettihwswBinarySensor(CoordinatorEntity, BinarySensorEntity): """Represent a binary sensor.""" def __init__(self, hass, coordinator, config_entry, name, sensor: Input): """Set initializing values.""" + super().__init__(coordinator) self._name = name self._sensor = sensor - self._coordinator = coordinator - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) @property def name(self): @@ -78,18 +75,4 @@ class ProgettihwswBinarySensor(BinarySensorEntity): @property def is_on(self): """Get sensor state.""" - return self._coordinator.data[self._sensor.id] - - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self._coordinator.last_update_success - - async def async_update(self): - """Update the state of binary sensor.""" - await self._coordinator.async_request_refresh() + return self.coordinator.data[self._sensor.id] diff --git a/homeassistant/components/progettihwsw/switch.py b/homeassistant/components/progettihwsw/switch.py index b6480d15c7b..5dc6005908f 100644 --- a/homeassistant/components/progettihwsw/switch.py +++ b/homeassistant/components/progettihwsw/switch.py @@ -7,7 +7,10 @@ from ProgettiHWSW.relay import Relay import async_timeout from homeassistant.components.switch import SwitchEntity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import setup_switch from .const import DEFAULT_POLLING_INTERVAL_SEC, DOMAIN @@ -54,35 +57,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(switches) -class ProgettihwswSwitch(SwitchEntity): +class ProgettihwswSwitch(CoordinatorEntity, SwitchEntity): """Represent a switch entity.""" def __init__(self, hass, coordinator, config_entry, name, switch: Relay): """Initialize the values.""" + super().__init__(coordinator) self._switch = switch self._name = name - self._coordinator = coordinator - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) async def async_turn_on(self, **kwargs): """Turn the switch on.""" await self._switch.control(True) - await self._coordinator.async_request_refresh() + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): """Turn the switch off.""" await self._switch.control(False) - await self._coordinator.async_request_refresh() + await self.coordinator.async_request_refresh() async def async_toggle(self, **kwargs): """Toggle the state of switch.""" await self._switch.toggle() - await self._coordinator.async_request_refresh() + await self.coordinator.async_request_refresh() @property def name(self): @@ -92,18 +89,4 @@ class ProgettihwswSwitch(SwitchEntity): @property def is_on(self): """Get switch state.""" - return self._coordinator.data[self._switch.id] - - @property - def should_poll(self): - """No need to poll. Coordinator notifies entity of updates.""" - return False - - @property - def available(self): - """Return if entity is available.""" - return self._coordinator.last_update_success - - async def async_update(self): - """Update the state of switch.""" - await self._coordinator.async_request_refresh() + return self.coordinator.data[self._switch.id] From f7e6b060a7e6b21998bb1c868d41842fe4a6084a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 30 Aug 2020 22:39:33 +0100 Subject: [PATCH 511/862] Make onvif username optional (#39415) --- homeassistant/components/onvif/config_flow.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index fada2d497a7..cbe6d03fa68 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -169,14 +169,15 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.onvif_config[CONF_PASSWORD] = user_input[CONF_PASSWORD] return await self.async_step_profiles() - # Password is optional and default empty due to some cameras not - # allowing you to change ONVIF user settings. - # See https://github.com/home-assistant/core/issues/35904 + # Username and Password are optional and default empty + # due to some cameras not allowing you to change ONVIF user settings. + # See https://github.com/home-assistant/core/issues/39182 + # and https://github.com/home-assistant/core/issues/35904 return self.async_show_form( step_id="auth", data_schema=vol.Schema( { - vol.Required(CONF_USERNAME): str, + vol.Optional(CONF_USERNAME, default=""): str, vol.Optional(CONF_PASSWORD, default=""): str, } ), From 75153dd4a3061f27674f4adbd9283e6c46534e66 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sun, 30 Aug 2020 19:15:09 -0400 Subject: [PATCH 512/862] Apply code review for insteon config flow (#39171) * Move options import to async_setup_entry * Add tests for insteon init * Move common constants to const * Clean up to adhear to standards * Create mock insteon device manager * Update for HA standards * Use keys and align to config_flow steps * Fix default port for hub v1 * Update doc string to represent function * Remove dump print commands * Add modem_type entry * Simplify dict key test * Setup platforms in async_setup_entry * Black * Black tests --- .coveragerc | 1 - homeassistant/components/insteon/__init__.py | 123 +++++----- .../components/insteon/config_flow.py | 10 +- homeassistant/components/insteon/schemas.py | 6 +- homeassistant/components/insteon/strings.json | 44 ++-- .../components/insteon/translations/en.json | 51 ++-- tests/components/insteon/const.py | 100 ++++++++ tests/components/insteon/mock_devices.py | 69 ++++++ tests/components/insteon/test_config_flow.py | 196 ++++----------- tests/components/insteon/test_init.py | 227 ++++++++++++++++++ 10 files changed, 552 insertions(+), 275 deletions(-) create mode 100644 tests/components/insteon/const.py create mode 100644 tests/components/insteon/mock_devices.py create mode 100644 tests/components/insteon/test_init.py diff --git a/.coveragerc b/.coveragerc index 1a7d1f7d394..2ec685df4ce 100644 --- a/.coveragerc +++ b/.coveragerc @@ -394,7 +394,6 @@ omit = homeassistant/components/ihc/* homeassistant/components/imap/sensor.py homeassistant/components/imap_email_content/sensor.py - homeassistant/components/insteon/__init__.py homeassistant/components/insteon/binary_sensor.py homeassistant/components/insteon/climate.py homeassistant/components/insteon/const.py diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index b567179fa4f..b0e6b8d10a2 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -6,7 +6,6 @@ from pyinsteon import async_close, async_connect, devices from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY from homeassistant.exceptions import ConfigEntryNotReady from .const import ( @@ -30,10 +29,19 @@ from .utils import ( ) _LOGGER = logging.getLogger(__name__) +OPTIONS = "options" -async def async_id_unknown_devices(config_dir): - """Send device ID commands to all unidentified devices.""" +async def async_get_device_config(hass, config_entry): + """Initiate the connection and services.""" + # Make a copy of addresses due to edge case where the list of devices could change during status update + # Cannot be done concurrently due to issues with the underlying protocol. + for address in list(devices): + try: + await devices[address].async_status() + except AttributeError: + pass + await devices.async_load(id_devices=1) for addr in devices: device = devices[addr] @@ -52,45 +60,7 @@ async def async_id_unknown_devices(config_dir): if not device.aldb.is_loaded or not flags: await device.async_read_config() - await devices.async_save(workdir=config_dir) - - -async def async_setup_platforms(hass, config_entry): - """Initiate the connection and services.""" - tasks = [ - hass.config_entries.async_forward_entry_setup(config_entry, component) - for component in INSTEON_COMPONENTS - ] - await asyncio.gather(*tasks) - - for address in devices: - device = devices[address] - platforms = get_device_platforms(device) - if ON_OFF_EVENTS in platforms: - add_on_off_event_device(hass, device) - - _LOGGER.debug("Insteon device count: %s", len(devices)) - register_new_device_callback(hass) - async_register_services(hass) - - device_registry = await hass.helpers.device_registry.async_get_registry() - device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, str(devices.modem.address))}, - manufacturer="Smart Home", - name=f"{devices.modem.description} {devices.modem.address}", - model=f"{devices.modem.model} (0x{devices.modem.cat:02x}, 0x{devices.modem.subcat:02x})", - sw_version=f"{devices.modem.firmware:02x} Engine Version: {devices.modem.engine_version}", - ) - - # Make a copy of addresses due to edge case where the list of devices could change during status update - # Cannot be done concurrently due to issues with the underlying protocol. - for address in list(devices): - try: - await devices[address].async_status() - except AttributeError: - pass - await async_id_unknown_devices(hass.config.config_dir) + await devices.async_save(workdir=hass.config.config_dir) async def close_insteon_connection(*args): @@ -98,30 +68,22 @@ async def close_insteon_connection(*args): await async_close() -async def async_import_config(hass, conf): - """Set up all of the config imported from yaml.""" - data, options = convert_yaml_to_config_flow(conf) - # Create a config entry with the connection data - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=data - ) - # If this is the first time we ran, update the config options - if result["type"] == RESULT_TYPE_CREATE_ENTRY and options: - entry = result["result"] - hass.config_entries.async_update_entry( - entry=entry, - options=options, - ) - return result - - async def async_setup(hass, config): """Set up the Insteon platform.""" if DOMAIN not in config: return True conf = config[DOMAIN] - hass.async_create_task(async_import_config(hass, conf)) + data, options = convert_yaml_to_config_flow(conf) + if options: + hass.data[DOMAIN] = {} + hass.data[DOMAIN][OPTIONS] = options + # Create a config entry with the connection data + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=data + ) + ) return True @@ -141,6 +103,19 @@ async def async_setup_entry(hass, entry): workdir=hass.config.config_dir, id_devices=0, load_modem_aldb=0 ) + # If options existed in YAML and have not already been saved to the config entry + # add them now + if ( + not entry.options + and entry.source == SOURCE_IMPORT + and hass.data.get(DOMAIN) + and hass.data[DOMAIN].get(OPTIONS) + ): + hass.config_entries.async_update_entry( + entry=entry, + options=hass.data[DOMAIN][OPTIONS], + ) + for device_override in entry.options.get(CONF_OVERRIDE, []): # Override the device default capabilities for a specific address address = device_override.get("address") @@ -163,5 +138,31 @@ async def async_setup_entry(hass, entry): ) device = devices.add_x10_device(housecode, unitcode, x10_type, steps) - asyncio.create_task(async_setup_platforms(hass, entry)) + for component in INSTEON_COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + for address in devices: + device = devices[address] + platforms = get_device_platforms(device) + if ON_OFF_EVENTS in platforms: + add_on_off_event_device(hass, device) + + _LOGGER.debug("Insteon device count: %s", len(devices)) + register_new_device_callback(hass) + async_register_services(hass) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, str(devices.modem.address))}, + manufacturer="Smart Home", + name=f"{devices.modem.description} {devices.modem.address}", + model=f"{devices.modem.model} (0x{devices.modem.cat:02x}, 0x{devices.modem.subcat:02x})", + sw_version=f"{devices.modem.firmware:02x} Engine Version: {devices.modem.engine_version}", + ) + + asyncio.create_task(async_get_device_config(hass, entry)) + return True diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index c6045893365..d8e17cfa03f 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -115,14 +115,10 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return InsteonOptionsFlowHandler(config_entry) async def async_step_user(self, user_input=None): - """For backward compatibility.""" - return await self.async_step_init(user_input=user_input) - - async def async_step_init(self, user_input=None): """Init the config flow.""" errors = {} if self._async_current_entries(): - return self.async_abort(reason="already_configured") + return self.async_abort(reason="single_instance_allowed") if user_input is not None: selection = user_input.get(MODEM_TYPE) @@ -134,7 +130,7 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): modem_types = [PLM, HUB1, HUB2] data_schema = vol.Schema({vol.Required(MODEM_TYPE): vol.In(modem_types)}) return self.async_show_form( - step_id="init", data_schema=data_schema, errors=errors + step_id="user", data_schema=data_schema, errors=errors ) async def async_step_plm(self, user_input=None): @@ -177,7 +173,7 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_info): """Import a yaml entry as a config entry.""" if self._async_current_entries(): - return self.async_abort(reason="already_configured") + return self.async_abort(reason="single_instance_allowed") if not await _async_connect(**import_info): return self.async_abort(reason="cannot_connect") return self.async_create_entry(title="", data=import_info) diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index a6c4b4627bc..adc7e945eba 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -271,11 +271,13 @@ def build_plm_schema(device=vol.UNDEFINED): def build_hub_schema( hub_version, host=vol.UNDEFINED, - port=PORT_HUB_V2, + port=vol.UNDEFINED, username=vol.UNDEFINED, password=vol.UNDEFINED, ): - """Build the Hub v2 schema for config flow.""" + """Build the Hub schema for config flow.""" + if port == vol.UNDEFINED: + port = PORT_HUB_V2 if hub_version == 2 else PORT_HUB_V1 schema = { vol.Required(CONF_HOST, default=host): str, vol.Required(CONF_PORT, default=port): int, diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index 8df77e6c157..d6690bd4860 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -1,48 +1,46 @@ { "config": { "step": { - "init": { + "user": { "title": "Insteon", "description": "Select the Insteon modem type.", "data": { - "plm": "PowerLink Modem (PLM)", - "hubv1": "Hub Version 1 (Pre-2014)", - "hubv2": "Hub Version 2" + "modem_type": "Modem type." } }, "plm": { "title": "Insteon PLM", "description": "Configure the Insteon PowerLink Modem (PLM).", "data": { - "device": "PLM device (i.e. /dev/ttyUSB0 or COM3)" + "device": "[%key:common::config_flow::data::usb_path%]" } }, - "hub1": { + "hubv1": { "title": "Insteon Hub Version 1", "description": "Configure the Insteon Hub Version 1 (pre-2014).", "data": { - "host": "Hub IP address", - "port": "IP port" + "host": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" } }, - "hub2": { + "hubv2": { "title": "Insteon Hub Version 2", "description": "Configure the Insteon Hub Version 2.", "data": { - "host": "Hub IP address", - "username": "Username", - "password": "Password", - "port": "IP port" + "host": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } } }, "error": { - "cannot_connect": "Failed to connect to the Insteon modem, please try again.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "select_single": "Select one option." }, "abort": { - "cannot_connect": "Unable to connect to the Insteon modem", - "already_configured": "An Insteon modem connection is already configured" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" } }, "options": { @@ -62,10 +60,10 @@ "title": "Insteon", "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app.", "data": { - "host": "New host name or IP address", - "username": "New username", - "password": "New password", - "port": "New port number" + "host": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" } }, "add_override": { @@ -103,13 +101,9 @@ } }, "error": { - "cannot_connect": "Failed to connect to the Insteon modem, please try again.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "select_single": "Select one option.", "input_error": "Invalid entries, please check your values." - }, - "abort": { - "cannot_connect": "Unable to connect to the Insteon modem", - "already_configured": "An Insteon modem connection is already configured" } } } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json index 92abb583a40..a37bab106e6 100644 --- a/homeassistant/components/insteon/translations/en.json +++ b/homeassistant/components/insteon/translations/en.json @@ -1,57 +1,48 @@ { "config": { "abort": { - "already_configured": "An Insteon modem connection is already configured", - "cannot_connect": "Unable to connect to the Insteon modem" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" }, "error": { - "cannot_connect": "Failed to connect to the Insteon modem, please try again.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "select_single": "Select one option." }, "step": { - "hub1": { + "hubv1": { "data": { - "host": "Hub IP address", - "port": "IP port" + "host": "[%key:common::config_flow::data::ip%]", + "port": "[%key:common::config_flow::data::port%]" }, "description": "Configure the Insteon Hub Version 1 (pre-2014).", "title": "Insteon Hub Version 1" }, - "hub2": { + "hubv2": { "data": { - "host": "Hub IP address", - "password": "Password", - "port": "IP port", - "username": "Username" + "host": "[%key:common::config_flow::data::ip%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]" }, "description": "Configure the Insteon Hub Version 2.", "title": "Insteon Hub Version 2" }, - "init": { - "data": { - "hubv1": "Hub Version 1 (Pre-2014)", - "hubv2": "Hub Version 2", - "plm": "PowerLink Modem (PLM)" - }, - "description": "Select the Insteon modem type.", - "title": "Insteon" - }, "plm": { "data": { - "device": "PLM device (i.e. /dev/ttyUSB0 or COM3)" + "device": "[%key:common::config_flow::data::usb_path%]" }, "description": "Configure the Insteon PowerLink Modem (PLM).", "title": "Insteon PLM" + }, + "user": { + "description": "Select the Insteon modem type.", + "title": "Insteon" } } }, "options": { - "abort": { - "already_configured": "An Insteon modem connection is already configured", - "cannot_connect": "Unable to connect to the Insteon modem" - }, "error": { - "cannot_connect": "Failed to connect to the Insteon modem, please try again.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "input_error": "Invalid entries, please check your values.", "select_single": "Select one option." }, @@ -77,10 +68,10 @@ }, "change_hub_config": { "data": { - "host": "New host name or IP address", - "password": "New password", - "port": "New port number", - "username": "New username" + "host": "[%key:common::config_flow::data::ip%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]" }, "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app.", "title": "Insteon" diff --git a/tests/components/insteon/const.py b/tests/components/insteon/const.py new file mode 100644 index 00000000000..ec59d94ba72 --- /dev/null +++ b/tests/components/insteon/const.py @@ -0,0 +1,100 @@ +"""Constants used for Insteon test cases.""" +from homeassistant.components.insteon.const import ( + CONF_CAT, + CONF_DIM_STEPS, + CONF_HOUSECODE, + CONF_HUB_VERSION, + CONF_OVERRIDE, + CONF_SUBCAT, + CONF_UNITCODE, + CONF_X10, + X10_PLATFORMS, +) +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE, + CONF_HOST, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_USERNAME, +) + +MOCK_HOSTNAME = "1.1.1.1" +MOCK_DEVICE = "/dev/ttyUSB55" +MOCK_USERNAME = "test-username" +MOCK_PASSWORD = "test-password" +MOCK_PORT = 4567 + +MOCK_ADDRESS = "1a2b3c" +MOCK_CAT = 0x02 +MOCK_SUBCAT = 0x1A + +MOCK_HOUSECODE = "c" +MOCK_UNITCODE_1 = 1 +MOCK_UNITCODE_2 = 2 +MOCK_X10_PLATFORM_1 = X10_PLATFORMS[0] +MOCK_X10_PLATFORM_2 = X10_PLATFORMS[2] +MOCK_X10_STEPS = 10 + +MOCK_USER_INPUT_PLM = { + CONF_DEVICE: MOCK_DEVICE, +} + +MOCK_USER_INPUT_HUB_V2 = { + CONF_HOST: MOCK_HOSTNAME, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_PORT: MOCK_PORT, +} + +MOCK_USER_INPUT_HUB_V1 = { + CONF_HOST: MOCK_HOSTNAME, + CONF_PORT: MOCK_PORT, +} + +MOCK_DEVICE_OVERRIDE_CONFIG = { + CONF_ADDRESS: MOCK_ADDRESS, + CONF_CAT: MOCK_CAT, + CONF_SUBCAT: MOCK_SUBCAT, +} + +MOCK_X10_CONFIG_1 = { + CONF_HOUSECODE: MOCK_HOUSECODE, + CONF_UNITCODE: MOCK_UNITCODE_1, + CONF_PLATFORM: MOCK_X10_PLATFORM_1, + CONF_DIM_STEPS: MOCK_X10_STEPS, +} + +MOCK_X10_CONFIG_2 = { + CONF_HOUSECODE: MOCK_HOUSECODE, + CONF_UNITCODE: MOCK_UNITCODE_2, + CONF_PLATFORM: MOCK_X10_PLATFORM_2, + CONF_DIM_STEPS: MOCK_X10_STEPS, +} + +MOCK_IMPORT_CONFIG_PLM = {CONF_PORT: MOCK_DEVICE} + +MOCK_IMPORT_MINIMUM_HUB_V2 = { + CONF_HOST: MOCK_HOSTNAME, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, +} +MOCK_IMPORT_MINIMUM_HUB_V1 = {CONF_HOST: MOCK_HOSTNAME, CONF_HUB_VERSION: 1} +MOCK_IMPORT_FULL_CONFIG_PLM = MOCK_IMPORT_CONFIG_PLM.copy() +MOCK_IMPORT_FULL_CONFIG_PLM[CONF_OVERRIDE] = [MOCK_DEVICE_OVERRIDE_CONFIG] +MOCK_IMPORT_FULL_CONFIG_PLM[CONF_X10] = [MOCK_X10_CONFIG_1, MOCK_X10_CONFIG_2] + +MOCK_IMPORT_FULL_CONFIG_HUB_V2 = MOCK_USER_INPUT_HUB_V2.copy() +MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_HUB_VERSION] = 2 +MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_OVERRIDE] = [MOCK_DEVICE_OVERRIDE_CONFIG] +MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_X10] = [MOCK_X10_CONFIG_1, MOCK_X10_CONFIG_2] + +MOCK_IMPORT_FULL_CONFIG_HUB_V1 = MOCK_USER_INPUT_HUB_V1.copy() +MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_HUB_VERSION] = 1 +MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_OVERRIDE] = [MOCK_DEVICE_OVERRIDE_CONFIG] +MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_X10] = [MOCK_X10_CONFIG_1, MOCK_X10_CONFIG_2] + +PATCH_CONNECTION = "homeassistant.components.insteon.config_flow.async_connect" +PATCH_ASYNC_SETUP = "homeassistant.components.insteon.async_setup" +PATCH_ASYNC_SETUP_ENTRY = "homeassistant.components.insteon.async_setup_entry" diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py new file mode 100644 index 00000000000..e57d06d210c --- /dev/null +++ b/tests/components/insteon/mock_devices.py @@ -0,0 +1,69 @@ +"""Mock devices object to test Insteon.""" +import logging + +from pyinsteon.address import Address +from pyinsteon.device_types import ( + GeneralController_MiniRemote_4, + Hub, + SwitchedLightingControl_SwitchLinc, +) + +from tests.async_mock import AsyncMock, MagicMock + +_LOGGER = logging.getLogger(__name__) + + +class MockSwitchLinc(SwitchedLightingControl_SwitchLinc): + """Mock SwitchLinc device.""" + + @property + def operating_flags(self): + """Return no operating flags to force properties to be checked.""" + return {} + + +class MockDevices: + """Mock devices class.""" + + def __init__(self, connected=True): + """Init the MockDevices class.""" + self._devices = {} + self.modem = None + self._connected = connected + self.async_save = AsyncMock() + self.add_x10_device = MagicMock() + self.set_id = MagicMock() + + def __getitem__(self, address): + """Return a a device from the device address.""" + return self._devices.get(address) + + def __iter__(self): + """Return an iterator of device addresses.""" + yield from self._devices + + def __len__(self): + """Return the number of devices.""" + return len(self._devices) + + def get(self, address): + """Return a device from an address or None if not found.""" + return self._devices.get(Address(address)) + + async def async_load(self, *args, **kwargs): + """Load the mock devices.""" + if self._connected: + addr0 = Address("AA.AA.AA") + addr1 = Address("11.11.11") + addr2 = Address("22.22.22") + addr3 = Address("33.33.33") + self._devices[addr0] = Hub(addr0) + self._devices[addr1] = MockSwitchLinc(addr1, 0x02, 0x00) + self._devices[addr2] = GeneralController_MiniRemote_4(addr2, 0x00, 0x00) + self._devices[addr3] = SwitchedLightingControl_SwitchLinc(addr3, 0x02, 0x00) + for device in [self._devices[addr] for addr in [addr1, addr2, addr3]]: + device.async_read_config = AsyncMock() + for device in [self._devices[addr] for addr in [addr2, addr3]]: + device.async_status = AsyncMock() + self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) + self.modem = self._devices[addr0] diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 9252f5edad3..f4a3806a891 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -1,7 +1,6 @@ """Test the config flow for the Insteon integration.""" from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.insteon import async_import_config from homeassistant.components.insteon.config_flow import ( HUB1, HUB2, @@ -24,7 +23,6 @@ from homeassistant.components.insteon.const import ( CONF_UNITCODE, CONF_X10, DOMAIN, - X10_PLATFORMS, ) from homeassistant.const import ( CONF_ADDRESS, @@ -37,79 +35,24 @@ from homeassistant.const import ( ) from homeassistant.helpers.typing import HomeAssistantType +from .const import ( + MOCK_HOSTNAME, + MOCK_IMPORT_CONFIG_PLM, + MOCK_IMPORT_MINIMUM_HUB_V1, + MOCK_IMPORT_MINIMUM_HUB_V2, + MOCK_PASSWORD, + MOCK_USER_INPUT_HUB_V1, + MOCK_USER_INPUT_HUB_V2, + MOCK_USER_INPUT_PLM, + MOCK_USERNAME, + PATCH_ASYNC_SETUP, + PATCH_ASYNC_SETUP_ENTRY, + PATCH_CONNECTION, +) + from tests.async_mock import patch from tests.common import MockConfigEntry -MOCK_HOSTNAME = "1.1.1.1" -MOCK_DEVICE = "/dev/ttyUSB55" -MOCK_USERNAME = "test-username" -MOCK_PASSWORD = "test-password" -MOCK_PORT = 4567 - -MOCK_ADDRESS = "1a2b3c" -MOCK_CAT = 0x02 -MOCK_SUBCAT = 0x1A - -MOCK_HOUSECODE = "c" -MOCK_UNITCODE = 6 -MOCK_X10_PLATFORM = X10_PLATFORMS[2] -MOCK_X10_STEPS = 10 - -MOCK_USER_INPUT_PLM = { - CONF_DEVICE: MOCK_DEVICE, -} - -MOCK_USER_INPUT_HUB_V2 = { - CONF_HOST: MOCK_HOSTNAME, - CONF_USERNAME: MOCK_USERNAME, - CONF_PASSWORD: MOCK_PASSWORD, - CONF_PORT: MOCK_PORT, -} - -MOCK_USER_INPUT_HUB_V1 = { - CONF_HOST: MOCK_HOSTNAME, - CONF_PORT: MOCK_PORT, -} - -MOCK_DEVICE_OVERRIDE_CONFIG = { - CONF_ADDRESS: MOCK_ADDRESS, - CONF_CAT: MOCK_CAT, - CONF_SUBCAT: MOCK_SUBCAT, -} - -MOCK_X10_CONFIG = { - CONF_HOUSECODE: MOCK_HOUSECODE, - CONF_UNITCODE: MOCK_UNITCODE, - CONF_PLATFORM: MOCK_X10_PLATFORM, - CONF_DIM_STEPS: MOCK_X10_STEPS, -} - -MOCK_IMPORT_CONFIG_PLM = {CONF_PORT: MOCK_DEVICE} - -MOCK_IMPORT_MINIMUM_HUB_V2 = { - CONF_HOST: MOCK_HOSTNAME, - CONF_USERNAME: MOCK_USERNAME, - CONF_PASSWORD: MOCK_PASSWORD, -} -MOCK_IMPORT_MINIMUM_HUB_V1 = {CONF_HOST: MOCK_HOSTNAME, CONF_HUB_VERSION: 1} -MOCK_IMPORT_FULL_CONFIG_PLM = MOCK_IMPORT_CONFIG_PLM.copy() -MOCK_IMPORT_FULL_CONFIG_PLM[CONF_OVERRIDE] = [MOCK_DEVICE_OVERRIDE_CONFIG] -MOCK_IMPORT_FULL_CONFIG_PLM[CONF_X10] = [MOCK_X10_CONFIG] - -MOCK_IMPORT_FULL_CONFIG_HUB_V2 = MOCK_USER_INPUT_HUB_V2.copy() -MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_HUB_VERSION] = 2 -MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_OVERRIDE] = [MOCK_DEVICE_OVERRIDE_CONFIG] -MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_X10] = [MOCK_X10_CONFIG] - -MOCK_IMPORT_FULL_CONFIG_HUB_V1 = MOCK_USER_INPUT_HUB_V1.copy() -MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_HUB_VERSION] = 1 -MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_OVERRIDE] = [MOCK_DEVICE_OVERRIDE_CONFIG] -MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_X10] = [MOCK_X10_CONFIG] - -PATCH_CONNECTION = "homeassistant.components.insteon.config_flow.async_connect" -PATCH_ASYNC_SETUP = "homeassistant.components.insteon.async_setup" -PATCH_ASYNC_SETUP_ENTRY = "homeassistant.components.insteon.async_setup_entry" - async def mock_successful_connection(*args, **kwargs): """Return a successful connection.""" @@ -122,7 +65,7 @@ async def mock_failed_connection(*args, **kwargs): async def _init_form(hass, modem_type): - """Run the init form.""" + """Run the user form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -173,7 +116,7 @@ async def test_fail_on_existing(hass: HomeAssistantType): context={"source": config_entries.SOURCE_USER}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "single_instance_allowed" async def test_form_select_plm(hass: HomeAssistantType): @@ -259,7 +202,9 @@ async def _import_config(hass, config): with patch(PATCH_CONNECTION, new=mock_successful_connection,), patch( PATCH_ASYNC_SETUP, return_value=True ), patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): - return await async_import_config(hass, config) + return await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) async def test_import_plm(hass: HomeAssistantType): @@ -271,71 +216,31 @@ async def test_import_plm(hass: HomeAssistantType): assert result["type"] == "create_entry" assert hass.config_entries.async_entries(DOMAIN) for entry in hass.config_entries.async_entries(DOMAIN): - assert entry.data == MOCK_USER_INPUT_PLM + assert entry.data == MOCK_IMPORT_CONFIG_PLM -async def test_import_plm_full(hass: HomeAssistantType): - """Test importing a full PLM config from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await _import_config(hass, MOCK_IMPORT_FULL_CONFIG_PLM) - assert result["type"] == "create_entry" - assert hass.config_entries.async_entries(DOMAIN) - for entry in hass.config_entries.async_entries(DOMAIN): - assert entry.data == MOCK_USER_INPUT_PLM - assert entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" - assert entry.options[CONF_OVERRIDE][0][CONF_CAT] == MOCK_CAT - assert entry.options[CONF_OVERRIDE][0][CONF_SUBCAT] == MOCK_SUBCAT - assert entry.options[CONF_X10][0][CONF_HOUSECODE] == MOCK_HOUSECODE - assert entry.options[CONF_X10][0][CONF_UNITCODE] == MOCK_UNITCODE - assert entry.options[CONF_X10][0][CONF_PLATFORM] == MOCK_X10_PLATFORM - assert entry.options[CONF_X10][0][CONF_DIM_STEPS] == MOCK_X10_STEPS +async def _options_init_form(hass, entry_id, step): + """Run the init options form.""" + with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): + result = await hass.config_entries.options.async_init(entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" -async def test_import_full_hub_v1(hass: HomeAssistantType): - """Test importing a full Hub v1 config from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - result = await _import_config(hass, MOCK_IMPORT_FULL_CONFIG_HUB_V1) - - assert result["type"] == "create_entry" - assert hass.config_entries.async_entries(DOMAIN) - for entry in hass.config_entries.async_entries(DOMAIN): - assert entry.data[CONF_HOST] == MOCK_HOSTNAME - assert entry.data[CONF_PORT] == MOCK_PORT - assert entry.data[CONF_HUB_VERSION] == 1 - assert CONF_USERNAME not in entry.data - assert CONF_PASSWORD not in entry.data - assert CONF_OVERRIDE not in entry.data - assert CONF_X10 not in entry.data - assert entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" - assert entry.options[CONF_X10][0][CONF_HOUSECODE] == MOCK_HOUSECODE - - -async def test_import_full_hub_v2(hass: HomeAssistantType): - """Test importing a full Hub v2 config from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - result = await _import_config(hass, MOCK_IMPORT_FULL_CONFIG_HUB_V2) - - assert result["type"] == "create_entry" - assert hass.config_entries.async_entries(DOMAIN) - for entry in hass.config_entries.async_entries(DOMAIN): - assert entry.data[CONF_HOST] == MOCK_HOSTNAME - assert entry.data[CONF_PORT] == MOCK_PORT - assert entry.data[CONF_USERNAME] == MOCK_USERNAME - assert entry.data[CONF_PASSWORD] == MOCK_PASSWORD - assert entry.data[CONF_HUB_VERSION] == 2 - assert CONF_OVERRIDE not in entry.data - assert CONF_X10 not in entry.data - assert entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" - assert entry.options[CONF_X10][0][CONF_HOUSECODE] == MOCK_HOUSECODE + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + {step: True}, + ) + return result2 async def test_import_min_hub_v2(hass: HomeAssistantType): """Test importing a minimum Hub v2 config from yaml.""" await setup.async_setup_component(hass, "persistent_notification", {}) - result = await _import_config(hass, MOCK_IMPORT_MINIMUM_HUB_V2) + result = await _import_config( + hass, {**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2} + ) assert result["type"] == "create_entry" assert hass.config_entries.async_entries(DOMAIN) @@ -351,7 +256,9 @@ async def test_import_min_hub_v1(hass: HomeAssistantType): """Test importing a minimum Hub v1 config from yaml.""" await setup.async_setup_component(hass, "persistent_notification", {}) - result = await _import_config(hass, MOCK_IMPORT_MINIMUM_HUB_V1) + result = await _import_config( + hass, {**MOCK_IMPORT_MINIMUM_HUB_V1, CONF_PORT: 9761, CONF_HUB_VERSION: 1} + ) assert result["type"] == "create_entry" assert hass.config_entries.async_entries(DOMAIN) @@ -372,9 +279,11 @@ async def test_import_existing(hass: HomeAssistantType): config_entry.add_to_hass(hass) assert config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED - result = await _import_config(hass, MOCK_IMPORT_MINIMUM_HUB_V2) + result = await _import_config( + hass, {**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "single_instance_allowed" async def test_import_failed_connection(hass: HomeAssistantType): @@ -384,27 +293,16 @@ async def test_import_failed_connection(hass: HomeAssistantType): with patch(PATCH_CONNECTION, new=mock_failed_connection,), patch( PATCH_ASYNC_SETUP, return_value=True ), patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): - result = await async_import_config(hass, MOCK_IMPORT_MINIMUM_HUB_V2) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" -async def _options_init_form(hass, entry_id, step): - """Run the init options form.""" - with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): - result = await hass.config_entries.options.async_init(entry_id) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - {step: True}, - ) - return result2 - - async def _options_form(hass, flow_id, user_input): """Test an options form.""" diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py new file mode 100644 index 00000000000..eed1ee76e8c --- /dev/null +++ b/tests/components/insteon/test_init.py @@ -0,0 +1,227 @@ +"""Test the init file for the Insteon component.""" +import asyncio +import logging + +from pyinsteon.address import Address + +from homeassistant.components import insteon +from homeassistant.components.insteon.const import ( + CONF_CAT, + CONF_OVERRIDE, + CONF_SUBCAT, + CONF_X10, + DOMAIN, + PORT_HUB_V1, + PORT_HUB_V2, +) +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE, + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component + +from .const import ( + MOCK_ADDRESS, + MOCK_CAT, + MOCK_IMPORT_CONFIG_PLM, + MOCK_IMPORT_FULL_CONFIG_HUB_V1, + MOCK_IMPORT_FULL_CONFIG_HUB_V2, + MOCK_IMPORT_FULL_CONFIG_PLM, + MOCK_IMPORT_MINIMUM_HUB_V1, + MOCK_IMPORT_MINIMUM_HUB_V2, + MOCK_SUBCAT, + MOCK_USER_INPUT_PLM, + PATCH_CONNECTION, +) +from .mock_devices import MockDevices + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +async def mock_successful_connection(*args, **kwargs): + """Return a successful connection.""" + return True + + +async def mock_failed_connection(*args, **kwargs): + """Return a failed connection.""" + raise ConnectionError("Connection failed") + + +async def test_setup_entry(hass: HomeAssistantType): + """Test setting up the entry.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + + with patch.object( + insteon, "async_connect", new=mock_successful_connection + ), patch.object(insteon, "async_close") as mock_close, patch.object( + insteon, "devices", new=MockDevices() + ): + assert await async_setup_component( + hass, + insteon.DOMAIN, + {}, + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + # pylint: disable=no-member + assert insteon.devices.async_save.call_count == 1 + assert mock_close.called + + +async def test_import_plm(hass: HomeAssistantType): + """Test setting up the entry from YAML to a PLM.""" + config = {} + config[DOMAIN] = MOCK_IMPORT_CONFIG_PLM + + with patch.object( + insteon, "async_connect", new=mock_successful_connection + ), patch.object(insteon, "close_insteon_connection"), patch.object( + insteon, "devices", new=MockDevices() + ), patch( + PATCH_CONNECTION, new=mock_successful_connection + ): + assert await async_setup_component( + hass, + insteon.DOMAIN, + config, + ) + await hass.async_block_till_done() + await asyncio.sleep(0.01) + assert hass.config_entries.async_entries(DOMAIN) + data = hass.config_entries.async_entries(DOMAIN)[0].data + assert data[CONF_DEVICE] == MOCK_IMPORT_CONFIG_PLM[CONF_PORT] + assert CONF_PORT not in data + + +async def test_import_hub1(hass: HomeAssistantType): + """Test setting up the entry from YAML to a hub v1.""" + config = {} + config[DOMAIN] = MOCK_IMPORT_MINIMUM_HUB_V1 + + with patch.object( + insteon, "async_connect", new=mock_successful_connection + ), patch.object(insteon, "close_insteon_connection"), patch.object( + insteon, "devices", new=MockDevices() + ), patch( + PATCH_CONNECTION, new=mock_successful_connection + ): + assert await async_setup_component( + hass, + insteon.DOMAIN, + config, + ) + await hass.async_block_till_done() + await asyncio.sleep(0.01) + assert hass.config_entries.async_entries(DOMAIN) + data = hass.config_entries.async_entries(DOMAIN)[0].data + assert data[CONF_HOST] == MOCK_IMPORT_FULL_CONFIG_HUB_V1[CONF_HOST] + assert data[CONF_PORT] == PORT_HUB_V1 + assert CONF_USERNAME not in data + assert CONF_PASSWORD not in data + + +async def test_import_hub2(hass: HomeAssistantType): + """Test setting up the entry from YAML to a hub v2.""" + config = {} + config[DOMAIN] = MOCK_IMPORT_MINIMUM_HUB_V2 + + with patch.object( + insteon, "async_connect", new=mock_successful_connection + ), patch.object(insteon, "close_insteon_connection"), patch.object( + insteon, "devices", new=MockDevices() + ), patch( + PATCH_CONNECTION, new=mock_successful_connection + ): + assert await async_setup_component( + hass, + insteon.DOMAIN, + config, + ) + await hass.async_block_till_done() + await asyncio.sleep(0.01) + assert hass.config_entries.async_entries(DOMAIN) + data = hass.config_entries.async_entries(DOMAIN)[0].data + assert data[CONF_HOST] == MOCK_IMPORT_FULL_CONFIG_HUB_V2[CONF_HOST] + assert data[CONF_PORT] == PORT_HUB_V2 + assert data[CONF_USERNAME] == MOCK_IMPORT_MINIMUM_HUB_V2[CONF_USERNAME] + assert data[CONF_PASSWORD] == MOCK_IMPORT_MINIMUM_HUB_V2[CONF_PASSWORD] + + +async def test_import_options(hass: HomeAssistantType): + """Test setting up the entry from YAML including options.""" + config = {} + config[DOMAIN] = MOCK_IMPORT_FULL_CONFIG_PLM + + with patch.object( + insteon, "async_connect", new=mock_successful_connection + ), patch.object(insteon, "close_insteon_connection"), patch.object( + insteon, "devices", new=MockDevices() + ), patch( + PATCH_CONNECTION, new=mock_successful_connection + ): + assert await async_setup_component( + hass, + insteon.DOMAIN, + config, + ) + await hass.async_block_till_done() + await asyncio.sleep(0.01) # Need to yield to async processes + # pylint: disable=no-member + assert insteon.devices.add_x10_device.call_count == 2 + assert insteon.devices.set_id.call_count == 1 + options = hass.config_entries.async_entries(DOMAIN)[0].options + assert len(options[CONF_OVERRIDE]) == 1 + assert options[CONF_OVERRIDE][0][CONF_ADDRESS] == str(Address(MOCK_ADDRESS)) + assert options[CONF_OVERRIDE][0][CONF_CAT] == MOCK_CAT + assert options[CONF_OVERRIDE][0][CONF_SUBCAT] == MOCK_SUBCAT + + assert len(options[CONF_X10]) == 2 + assert options[CONF_X10][0] == MOCK_IMPORT_FULL_CONFIG_PLM[CONF_X10][0] + assert options[CONF_X10][1] == MOCK_IMPORT_FULL_CONFIG_PLM[CONF_X10][1] + + +async def test_import_failed_connection(hass: HomeAssistantType): + """Test a failed connection in import does not create a config entry.""" + config = {} + config[DOMAIN] = MOCK_IMPORT_CONFIG_PLM + + with patch.object( + insteon, "async_connect", new=mock_failed_connection + ), patch.object(insteon, "async_close"), patch.object( + insteon, "devices", new=MockDevices(connected=False) + ): + assert await async_setup_component( + hass, + insteon.DOMAIN, + config, + ) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(DOMAIN) + + +async def test_setup_entry_failed_connection(hass: HomeAssistantType, caplog): + """Test setting up the entry with a failed connection.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + + with patch.object( + insteon, "async_connect", new=mock_failed_connection + ), patch.object(insteon, "devices", new=MockDevices(connected=False)): + assert await async_setup_component( + hass, + insteon.DOMAIN, + {}, + ) + assert "Could not connect to Insteon modem" in caplog.text From 2856915b6dd2597fcce9aceec1976dd6a4421bae Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 31 Aug 2020 00:03:01 +0000 Subject: [PATCH 513/862] [ci skip] Translation update --- .../components/insteon/translations/en.json | 63 ++++++++++++++----- .../components/nzbget/translations/ca.json | 36 +++++++++++ .../components/nzbget/translations/ru.json | 36 +++++++++++ .../nzbget/translations/zh-Hant.json | 36 +++++++++++ .../progettihwsw/translations/en.json | 40 ++++++------ .../components/sharkiq/translations/en.json | 39 +++++------- .../components/sharkiq/translations/ru.json | 20 ++++++ 7 files changed, 213 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/nzbget/translations/ca.json create mode 100644 homeassistant/components/nzbget/translations/ru.json create mode 100644 homeassistant/components/nzbget/translations/zh-Hant.json create mode 100644 homeassistant/components/sharkiq/translations/ru.json diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json index a37bab106e6..eec80f60b90 100644 --- a/homeassistant/components/insteon/translations/en.json +++ b/homeassistant/components/insteon/translations/en.json @@ -1,48 +1,83 @@ { "config": { "abort": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "already_configured": "An Insteon modem connection is already configured", + "cannot_connect": "Failed to connect", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_connect": "Failed to connect", "select_single": "Select one option." }, "step": { + "hub1": { + "data": { + "host": "Hub IP address", + "port": "IP port" + }, + "description": "Configure the Insteon Hub Version 1 (pre-2014).", + "title": "Insteon Hub Version 1" + }, + "hub2": { + "data": { + "host": "Hub IP address", + "password": "Password", + "port": "IP port", + "username": "Username" + }, + "description": "Configure the Insteon Hub Version 2.", + "title": "Insteon Hub Version 2" + }, "hubv1": { "data": { - "host": "[%key:common::config_flow::data::ip%]", - "port": "[%key:common::config_flow::data::port%]" + "host": "IP Address", + "port": "Port" }, "description": "Configure the Insteon Hub Version 1 (pre-2014).", "title": "Insteon Hub Version 1" }, "hubv2": { "data": { - "host": "[%key:common::config_flow::data::ip%]", - "password": "[%key:common::config_flow::data::password%]", - "port": "[%key:common::config_flow::data::port%]", - "username": "[%key:common::config_flow::data::username%]" + "host": "IP Address", + "password": "Password", + "port": "Port", + "username": "Username" }, "description": "Configure the Insteon Hub Version 2.", "title": "Insteon Hub Version 2" }, + "init": { + "data": { + "hubv1": "Hub Version 1 (Pre-2014)", + "hubv2": "Hub Version 2", + "plm": "PowerLink Modem (PLM)" + }, + "description": "Select the Insteon modem type.", + "title": "Insteon" + }, "plm": { "data": { - "device": "[%key:common::config_flow::data::usb_path%]" + "device": "USB Device Path" }, "description": "Configure the Insteon PowerLink Modem (PLM).", "title": "Insteon PLM" }, "user": { + "data": { + "modem_type": "Modem type." + }, "description": "Select the Insteon modem type.", "title": "Insteon" } } }, "options": { + "abort": { + "already_configured": "An Insteon modem connection is already configured", + "cannot_connect": "Unable to connect to the Insteon modem" + }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_connect": "Failed to connect", "input_error": "Invalid entries, please check your values.", "select_single": "Select one option." }, @@ -68,10 +103,10 @@ }, "change_hub_config": { "data": { - "host": "[%key:common::config_flow::data::ip%]", - "password": "[%key:common::config_flow::data::password%]", - "port": "[%key:common::config_flow::data::port%]", - "username": "[%key:common::config_flow::data::username%]" + "host": "IP Address", + "password": "Password", + "port": "Port", + "username": "Username" }, "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app.", "title": "Insteon" diff --git a/homeassistant/components/nzbget/translations/ca.json b/homeassistant/components/nzbget/translations/ca.json new file mode 100644 index 00000000000..535b56a3768 --- /dev/null +++ b/homeassistant/components/nzbget/translations/ca.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "ssl": "NZBGet utilitza un certificat SSL", + "username": "Nom d'usuari", + "verify_ssl": "NZBGet utilitza un certificat adequat" + }, + "title": "Connexi\u00f3 amb NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3 (segons)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/ru.json b/homeassistant/components/nzbget/translations/ru.json new file mode 100644 index 00000000000..93e44307ab8 --- /dev/null +++ b/homeassistant/components/nzbget/translations/ru.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "NZBGet \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", + "username": "\u041b\u043e\u0433\u0438\u043d", + "verify_ssl": "NZBGet \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" + }, + "title": "NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/zh-Hant.json b/homeassistant/components/nzbget/translations/zh-Hant.json new file mode 100644 index 00000000000..d2fc50ac26a --- /dev/null +++ b/homeassistant/components/nzbget/translations/zh-Hant.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "NZBGet\uff1a{name}", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "ssl": "NZBGet \u4f7f\u7528 SSL \u8a8d\u8b49", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "NZBGet \u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49" + }, + "title": "\u9023\u7dda\u81f3 NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387\uff08\u79d2\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/en.json b/homeassistant/components/progettihwsw/translations/en.json index 254620bcf8b..9add8609390 100644 --- a/homeassistant/components/progettihwsw/translations/en.json +++ b/homeassistant/components/progettihwsw/translations/en.json @@ -1,22 +1,21 @@ { "config": { "error": { - "cannot_connect": "Cannot connect to the board.", - "unknown": "Unknown error.", + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error", "wrong_info_relay_modes": "Relay mode selection must be monostable or bistable." }, "step": { - "user": { - "title": "Set up board", - "data": { - "host": "Host", - "port": "Port Number" - } - }, "relay_modes": { - "title": "Set up relays", "data": { "relay_1": "Relay 1", + "relay_10": "Relay 10", + "relay_11": "Relay 11", + "relay_12": "Relay 12", + "relay_13": "Relay 13", + "relay_14": "Relay 14", + "relay_15": "Relay 15", + "relay_16": "Relay 16", "relay_2": "Relay 2", "relay_3": "Relay 3", "relay_4": "Relay 4", @@ -24,17 +23,18 @@ "relay_6": "Relay 6", "relay_7": "Relay 7", "relay_8": "Relay 8", - "relay_9": "Relay 9", - "relay_10": "Relay 10", - "relay_11": "Relay 11", - "relay_12": "Relay 12", - "relay_13": "Relay 13", - "relay_14": "Relay 14", - "relay_15": "Relay 15", - "relay_16": "Relay 16" - } + "relay_9": "Relay 9" + }, + "title": "Set up relays" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Set up board" } } }, "title": "ProgettiHWSW Automation" - } \ No newline at end of file +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/en.json b/homeassistant/components/sharkiq/translations/en.json index 3bd9bb2e46e..331c12402f1 100644 --- a/homeassistant/components/sharkiq/translations/en.json +++ b/homeassistant/components/sharkiq/translations/en.json @@ -1,27 +1,20 @@ { - "title": "Shark IQ", - "config": { - "step": { - "init": { - "data": { - "username": "Username", - "password": "Password" + "config": { + "abort": { + "already_configured_account": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } } - }, - "user": { - "data": { - "username": "Username", - "password": "Password" - } - } - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" } - } } \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/ru.json b/homeassistant/components/sharkiq/translations/ru.json new file mode 100644 index 00000000000..0c8d369dbec --- /dev/null +++ b/homeassistant/components/sharkiq/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured_account": "\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": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + } +} \ No newline at end of file From e1c7c3fdb214e997154ae6ac8b2b2e1ccbcf7e1c Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 30 Aug 2020 21:04:49 -0500 Subject: [PATCH 514/862] Ensure patching applies while testing marytts (#39490) * ensure patching applies while testing marytts * Update test_tts.py --- tests/components/marytts/test_tts.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index b5681460b75..ce2092dd607 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -77,14 +77,14 @@ class TestTTSMaryTTSPlatform: tts.ATTR_MESSAGE: "HomeAssistant", }, ) - self.hass.block_till_done() - - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 + self.hass.block_till_done() mock_speak.assert_called_once() mock_speak.assert_called_with("HomeAssistant", {}) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 + def test_service_say_with_effect(self): """Test service call say with effects.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) @@ -108,14 +108,14 @@ class TestTTSMaryTTSPlatform: tts.ATTR_MESSAGE: "HomeAssistant", }, ) - self.hass.block_till_done() - - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 + self.hass.block_till_done() mock_speak.assert_called_once() mock_speak.assert_called_with("HomeAssistant", {"Volume": "amount:2.0;"}) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 + def test_service_say_http_error(self): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) @@ -137,8 +137,7 @@ class TestTTSMaryTTSPlatform: tts.ATTR_MESSAGE: "HomeAssistant", }, ) - self.hass.block_till_done() - - assert len(calls) == 0 + self.hass.block_till_done() mock_speak.assert_called_once() + assert len(calls) == 0 From b71cbd2033097b3ebeeecb6ebe6f2c2b2d70ad65 Mon Sep 17 00:00:00 2001 From: Eugene Prystupa Date: Sun, 30 Aug 2020 22:10:15 -0400 Subject: [PATCH 515/862] Fix a problem with set_speed(off) when direct HA API for set speed is called (#39488) --- homeassistant/components/bond/fan.py | 4 ++++ tests/components/bond/test_fan.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 14a5f84c8b1..19e345b7e23 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -102,6 +102,10 @@ class BondFan(BondEntity, FanEntity): """Set the desired speed for the fan.""" _LOGGER.debug("async_set_speed called with speed %s", speed) + if speed == SPEED_OFF: + await self.async_turn_off() + return + max_speed = self._device.props.get("max_speed", 3) if speed == SPEED_LOW: bond_speed = 1 diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 0e0e980c39b..1adea282a30 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -8,11 +8,14 @@ from homeassistant import core from homeassistant.components import fan from homeassistant.components.fan import ( ATTR_DIRECTION, + ATTR_SPEED, ATTR_SPEED_LIST, DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN as FAN_DOMAIN, SERVICE_SET_DIRECTION, + SERVICE_SET_SPEED, + SPEED_OFF, ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers.entity_registry import EntityRegistry @@ -156,6 +159,24 @@ async def test_turn_on_fan_with_off_speed(hass: core.HomeAssistant): mock_turn_off.assert_called_with("test-device-id", Action.turn_off()) +async def test_set_speed_off(hass: core.HomeAssistant): + """Tests that set_speed(off) command delegates to turn off API.""" + await setup_platform( + hass, FAN_DOMAIN, ceiling_fan("name-1"), bond_device_id="test-device-id" + ) + + with patch_bond_action() as mock_turn_off, patch_bond_device_state(): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + service_data={ATTR_ENTITY_ID: "fan.name_1", ATTR_SPEED: SPEED_OFF}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_turn_off.assert_called_with("test-device-id", Action.turn_off()) + + async def test_turn_off_fan(hass: core.HomeAssistant): """Tests that turn off command delegates to API.""" await setup_platform( From 4688171ce0c72add8ef6a0632fe16f4f55a72fcb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Aug 2020 09:25:39 +0200 Subject: [PATCH 516/862] Add cache headers to picture integration (#39402) --- homeassistant/components/image/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index d08be3e9127..c68df580643 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -11,6 +11,7 @@ from aiohttp import hdrs, web from aiohttp.web_request import FileField import voluptuous as vol +from homeassistant.components.http.static import CACHE_HEADERS from homeassistant.components.http.view import HomeAssistantView from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback @@ -197,7 +198,8 @@ class ImageServeView(HomeAssistantView): ) return web.FileResponse( - target_file, headers={hdrs.CONTENT_TYPE: image_info["content_type"]} + target_file, + headers={**CACHE_HEADERS, hdrs.CONTENT_TYPE: image_info["content_type"]}, ) From a1678368cf58efaf2c1a9efbd6c84a4871948335 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Aug 2020 10:20:39 +0200 Subject: [PATCH 517/862] Upgrade pydocstyle to 5.1.1 (#39492) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bc48c781690..8af0a7553e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==5.1.0 + - pydocstyle==5.1.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 48ee770b1d9..84e0f4e9905 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -6,6 +6,6 @@ codespell==1.17.1 flake8-docstrings==1.5.0 flake8==3.8.3 isort==5.4.2 -pydocstyle==5.1.0 +pydocstyle==5.1.1 pyupgrade==2.7.2 yamllint==1.24.2 From 09efbc569fb9164aa581ebb559054d1b46e2657f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 31 Aug 2020 09:25:25 +0100 Subject: [PATCH 518/862] Fix oauth2 template by updating outdated method name (#39486) --- script/scaffold/templates/config_flow_oauth2/integration/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py index bf0d9fd8817..14f7d60096c 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/api.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py @@ -52,7 +52,7 @@ class AsyncConfigEntryAuth(my_pypi_package.AbstractAuth): async def async_get_access_token(self): """Return a valid access token.""" - if not self._oauth_session.is_valid: + if not self._oauth_session.valid_token: await self._oauth_session.async_ensure_token_valid() return self._oauth_session.token From 97e1be98dfc9801fb98012081f3875e202a3c9be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Aug 2020 10:28:52 +0200 Subject: [PATCH 519/862] Upgrade sentry-sdk to 0.17.1 (#39495) --- homeassistant/components/sentry/__init__.py | 1 - homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sentry/test_init.py | 2 -- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 62f7af8b900..eecac0281e6 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -75,7 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: tracing = {} if entry.options.get(CONF_TRACING): tracing = { - "traceparent_v2": True, "traces_sample_rate": entry.options.get( CONF_TRACING_SAMPLE_RATE, DEFAULT_TRACING_SAMPLE_RATE ), diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 5ed4649b7c4..66894bfb2e3 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,6 +3,6 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==0.16.5"], + "requirements": ["sentry-sdk==0.17.1"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3c17612b7bf..07ba08104f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1959,7 +1959,7 @@ sense-hat==2.2.0 sense_energy==0.7.2 # homeassistant.components.sentry -sentry-sdk==0.16.5 +sentry-sdk==0.17.1 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21d610c2aba..4f63650066a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -908,7 +908,7 @@ samsungtvws[websocket]==1.4.0 sense_energy==0.7.2 # homeassistant.components.sentry -sentry-sdk==0.16.5 +sentry-sdk==0.17.1 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/tests/components/sentry/test_init.py b/tests/components/sentry/test_init.py index 6c3555092f0..95bda9738b8 100644 --- a/tests/components/sentry/test_init.py +++ b/tests/components/sentry/test_init.py @@ -98,11 +98,9 @@ async def test_setup_entry_with_tracing(hass: HomeAssistant) -> None: "integrations", "release", "before_send", - "traceparent_v2", "traces_sample_rate", } assert call_args["traces_sample_rate"] == 0.5 - assert call_args["traceparent_v2"] @pytest.mark.parametrize( From 00eb23b43f576f955d2058e420b67d32c27802cd Mon Sep 17 00:00:00 2001 From: Denys Dovhan Date: Mon, 31 Aug 2020 11:51:03 +0300 Subject: [PATCH 520/862] Allow loading Lovelace dashboards not only from root (#37561) --- homeassistant/components/lovelace/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 2a8c97f3d4e..d67298e4a78 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType from homeassistant.loader import async_get_integration -from homeassistant.util import sanitize_filename +from homeassistant.util import sanitize_path from . import dashboard, resources, websocket from .const import ( @@ -46,7 +46,7 @@ YAML_DASHBOARD_SCHEMA = vol.Schema( { **DASHBOARD_BASE_CREATE_FIELDS, vol.Required(CONF_MODE): MODE_YAML, - vol.Required(CONF_FILENAME): vol.All(cv.string, sanitize_filename), + vol.Required(CONF_FILENAME): vol.All(cv.string, sanitize_path), } ) From 190611a0799fc1e07878bb62c2bbeacf91e7406a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Aug 2020 10:51:30 +0200 Subject: [PATCH 521/862] Detect comments in jinja templates (#39496) --- homeassistant/helpers/template.py | 2 +- tests/helpers/test_template.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index fddd32c8760..b9dc854cd2b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -52,7 +52,7 @@ _RE_GET_ENTITIES = re.compile( re.I | re.M, ) -_RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{") +_RE_JINJA_DELIMITERS = re.compile(r"\{%|\{\{|\{#") _RESERVED_NAMES = {"contextfunction", "evalcontextfunction", "environmentfunction"} diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 3b293c106f9..b0643d1addf 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2279,3 +2279,12 @@ async def test_cache_garbage_collection(): assert not template._NO_HASS_ENV.template_cache.get( template_string ) # pylint: disable=protected-access + + +def test_is_template_string(): + """Test is template string.""" + assert template.is_template_string("{{ x }}") is True + assert template.is_template_string("{% if x == 2 %}1{% else %}0{%end if %}") is True + assert template.is_template_string("{# a comment #} Hey") is True + assert template.is_template_string("1") is False + assert template.is_template_string("Some Text") is False From bba8b8e7594f8701c9be7d4d931ba9bc189dc171 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Mon, 31 Aug 2020 11:38:52 +0200 Subject: [PATCH 522/862] Add support for a dedicated weather station within KNX (#39476) * Adds support for a dedicated weather station within KNX * Review * Change config values to comply with the naming of the other platforms --- homeassistant/components/knx/__init__.py | 4 ++ homeassistant/components/knx/const.py | 1 + homeassistant/components/knx/factory.py | 36 ++++++++++++++ homeassistant/components/knx/schema.py | 43 ++++++++++++++++ homeassistant/components/knx/weather.py | 62 ++++++++++++++++++++++++ 5 files changed, 146 insertions(+) create mode 100644 homeassistant/components/knx/weather.py diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index cfa01d93d97..7e56d2a955a 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -37,6 +37,7 @@ from .schema import ( SceneSchema, SensorSchema, SwitchSchema, + WeatherSchema, ) _LOGGER = logging.getLogger(__name__) @@ -102,6 +103,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(SupportedPlatforms.scene.value): vol.All( cv.ensure_list, [SceneSchema.SCHEMA] ), + vol.Optional(SupportedPlatforms.weather.value): vol.All( + cv.ensure_list, [WeatherSchema.SCHEMA] + ), } ) }, diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index f259638457a..a81fc526415 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -39,6 +39,7 @@ class SupportedPlatforms(Enum): notify = "notify" scene = "scene" sensor = "sensor" + weather = "weather" # Map KNX operation modes to HA modes. This list might not be complete. diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index a596875c27b..42c4dd675f5 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -12,6 +12,7 @@ from xknx.devices import ( Scene as XknxScene, Sensor as XknxSensor, Switch as XknxSwitch, + Weather as XknxWeather, ) from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, CONF_TYPE @@ -28,6 +29,7 @@ from .schema import ( SceneSchema, SensorSchema, SwitchSchema, + WeatherSchema, ) @@ -62,6 +64,9 @@ def create_knx_device( if platform is SupportedPlatforms.binary_sensor: return _create_binary_sensor(hass, knx_module, config) + if platform is SupportedPlatforms.weather: + return _create_weather(knx_module, config) + def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: """Return a KNX Cover device to be used within XKNX.""" @@ -263,3 +268,34 @@ def _create_binary_sensor( reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), actions=actions, ) + + +def _create_weather(knx_module: XKNX, config: ConfigType) -> XknxWeather: + """Return a KNX weather device to be used within XKNX.""" + return XknxWeather( + knx_module, + name=config[CONF_NAME], + sync_state=config[WeatherSchema.CONF_SYNC_STATE], + expose_sensors=config[WeatherSchema.CONF_KNX_EXPOSE_SENSORS], + group_address_temperature=config[WeatherSchema.CONF_KNX_TEMPERATURE_ADDRESS], + group_address_brightness_south=config.get( + WeatherSchema.CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS + ), + group_address_brightness_east=config.get( + WeatherSchema.CONF_KNX_BRIGHTNESS_EAST_ADDRESS + ), + group_address_brightness_west=config.get( + WeatherSchema.CONF_KNX_BRIGHTNESS_WEST_ADDRESS + ), + group_address_wind_speed=config.get(WeatherSchema.CONF_KNX_WIND_SPEED_ADDRESS), + group_address_rain_alarm=config.get(WeatherSchema.CONF_KNX_RAIN_ALARM_ADDRESS), + group_address_frost_alarm=config.get( + WeatherSchema.CONF_KNX_FROST_ALARM_ADDRESS + ), + group_address_wind_alarm=config.get(WeatherSchema.CONF_KNX_WIND_ALARM_ADDRESS), + group_address_day_night=config.get(WeatherSchema.CONF_KNX_DAY_NIGHT_ADDRESS), + group_address_air_pressure=config.get( + WeatherSchema.CONF_KNX_AIR_PRESSURE_ADDRESS + ), + group_address_humidity=config.get(WeatherSchema.CONF_KNX_HUMIDITY_ADDRESS), + ) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 3b9436a4eee..a436f2dcdc8 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -340,3 +340,46 @@ class SceneSchema: vol.Required(CONF_SCENE_NUMBER): cv.positive_int, } ) + + +class WeatherSchema: + """Voluptuous schema for KNX weather station.""" + + CONF_SYNC_STATE = CONF_SYNC_STATE + CONF_KNX_TEMPERATURE_ADDRESS = "address_temperature" + CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS = "address_brightness_south" + CONF_KNX_BRIGHTNESS_EAST_ADDRESS = "address_brightness_east" + CONF_KNX_BRIGHTNESS_WEST_ADDRESS = "address_brightness_west" + CONF_KNX_WIND_SPEED_ADDRESS = "address_wind_speed" + CONF_KNX_RAIN_ALARM_ADDRESS = "address_rain_alarm" + CONF_KNX_FROST_ALARM_ADDRESS = "address_frost_alarm" + CONF_KNX_WIND_ALARM_ADDRESS = "address_wind_alarm" + CONF_KNX_DAY_NIGHT_ADDRESS = "address_day_night" + CONF_KNX_AIR_PRESSURE_ADDRESS = "address_air_pressure" + CONF_KNX_HUMIDITY_ADDRESS = "address_humidity" + CONF_KNX_EXPOSE_SENSORS = "expose_sensors" + + DEFAULT_NAME = "KNX Weather Station" + + SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SYNC_STATE, default=True): vol.Any( + vol.All(vol.Coerce(int), vol.Range(min=2, max=1440)), + cv.boolean, + cv.string, + ), + vol.Optional(CONF_KNX_EXPOSE_SENSORS, default=False): cv.boolean, + vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): cv.string, + vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): cv.string, + vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): cv.string, + vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): cv.string, + vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): cv.string, + vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): cv.string, + vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): cv.string, + vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): cv.string, + vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): cv.string, + vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): cv.string, + vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): cv.string, + } + ) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py new file mode 100644 index 00000000000..aed376ec066 --- /dev/null +++ b/homeassistant/components/knx/weather.py @@ -0,0 +1,62 @@ +"""Support for KNX/IP weather station.""" +from xknx.devices import Weather as XknxWeather + +from homeassistant.components.weather import WeatherEntity +from homeassistant.const import TEMP_CELSIUS + +from .const import DATA_KNX + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the scenes for KNX platform.""" + entities = [] + for device in hass.data[DATA_KNX].xknx.devices: + if isinstance(device, XknxWeather): + entities.append(KNXWeather(device)) + async_add_entities(entities) + + +class KNXWeather(WeatherEntity): + """Representation of a KNX weather device.""" + + def __init__(self, device: XknxWeather): + """Initialize of a KNX sensor.""" + self.device = device + + @property + def temperature(self): + """Return current temperature.""" + return self.device.temperature + + @property + def temperature_unit(self): + """Return temperature unit.""" + return TEMP_CELSIUS + + @property + def pressure(self): + """Return current air pressure.""" + # KNX returns pA - HA requires hPa + return ( + self.device.air_pressure / 100 + if self.device.air_pressure is not None + else None + ) + + @property + def condition(self): + """Return current weather condition.""" + return self.device.ha_current_state().value + + @property + def humidity(self): + """Return current humidity.""" + return self.device.humidity if self.device.humidity is not None else None + + @property + def wind_speed(self): + """Return current wind speed in km/h.""" + # KNX only supports wind speed in m/s + return ( + self.device.wind_speed * 3.6 if self.device.wind_speed is not None else None + ) From 84205a9a5731d8ee9c001edd98b26518b82f81a9 Mon Sep 17 00:00:00 2001 From: arunderwood Date: Mon, 31 Aug 2020 05:48:55 -0400 Subject: [PATCH 523/862] route53 - support updating base domain (#39264) --- homeassistant/components/route53/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index 5355ed15f38..1061b7979ba 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -66,6 +66,12 @@ def setup(hass, config): return True +def _get_fqdn(record, domain): + if record == ".": + return domain + return f"{record}.{domain}" + + def _update_route53( aws_access_key_id: str, aws_secret_access_key: str, @@ -98,7 +104,7 @@ def _update_route53( { "Action": "UPSERT", "ResourceRecordSet": { - "Name": f"{record}.{domain}", + "Name": _get_fqdn(record, domain), "Type": "A", "TTL": ttl, "ResourceRecords": [{"Value": ipaddress}], From f187091594b08e64e5c34bb84b729fd01f01f417 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 31 Aug 2020 13:27:57 +0200 Subject: [PATCH 524/862] Add Xiaomi Miio gateway illuminance sensor and gateway light (#37959) Co-authored-by: Xiaonan Shen Co-authored-by: Teemu R. --- .../components/xiaomi_miio/__init__.py | 2 +- .../xiaomi_miio/alarm_control_panel.py | 6 +- .../components/xiaomi_miio/config_flow.py | 3 +- homeassistant/components/xiaomi_miio/light.py | 127 ++++++++++++++++++ .../components/xiaomi_miio/sensor.py | 86 +++++++++++- 5 files changed, 217 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 63a8bef54af..6957862ab7a 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -11,7 +11,7 @@ from .gateway import ConnectXiaomiGateway _LOGGER = logging.getLogger(__name__) -GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor"] +GATEWAY_PLATFORMS = ["alarm_control_panel", "sensor", "light"] async def async_setup(hass: core.HomeAssistant, config: dict): diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index 7df9dc54e5a..89fcb45b864 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -3,7 +3,7 @@ from functools import partial import logging -from miio import DeviceException +from miio.gateway import GatewayException from homeassistant.components.alarm_control_panel import ( SUPPORT_ALARM_ARM_AWAY, @@ -103,7 +103,7 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): partial(func, *args, **kwargs) ) _LOGGER.debug("Response received from miio device: %s", result) - except DeviceException as exc: + except GatewayException as exc: _LOGGER.error(mask_error, exc) async def async_alarm_arm_away(self, code=None): @@ -122,7 +122,7 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._gateway.alarm.status) - except DeviceException as ex: + except GatewayException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 7de55835916..6a62fbead13 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -17,6 +17,7 @@ CONF_FLOW_TYPE = "config_flow_device" CONF_GATEWAY = "gateway" DEFAULT_GATEWAY_NAME = "Xiaomi Gateway" ZEROCONF_GATEWAY = "lumi-gateway" +ZEROCONF_ACPARTNER = "lumi-acpartner" GATEWAY_SETTINGS = { vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), @@ -61,7 +62,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_xiaomi_miio") # Check which device is discovered. - if name.startswith(ZEROCONF_GATEWAY): + if name.startswith(ZEROCONF_GATEWAY) or name.startswith(ZEROCONF_ACPARTNER): unique_id = format_mac(mac_address) await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: self.host}) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index a148b98ee22..29c80faa892 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -14,6 +14,12 @@ from miio import ( # pylint: disable=import-error PhilipsEyecare, PhilipsMoonlight, ) +from miio.gateway import ( + GATEWAY_MODEL_AC_V1, + GATEWAY_MODEL_AC_V2, + GATEWAY_MODEL_AC_V3, + GatewayException, +) import voluptuous as vol from homeassistant.components.light import ( @@ -31,6 +37,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import color, dt +from .config_flow import CONF_FLOW_TYPE, CONF_GATEWAY from .const import ( DOMAIN, SERVICE_EYECARE_MODE_OFF, @@ -123,6 +130,25 @@ SERVICE_TO_METHOD = { } +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Xiaomi light from a config entry.""" + entities = [] + + if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: + gateway = hass.data[DOMAIN][config_entry.entry_id] + # Gateway light + if gateway.model not in [ + GATEWAY_MODEL_AC_V1, + GATEWAY_MODEL_AC_V2, + GATEWAY_MODEL_AC_V3, + ]: + entities.append( + XiaomiGatewayLight(gateway, config_entry.title, config_entry.unique_id) + ) + + async_add_entities(entities, update_before_add=True) + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the light from config.""" if DATA_KEY not in hass.data: @@ -938,3 +964,104 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): async def async_set_delayed_turn_off(self, time_period: timedelta): """Set delayed turn off. Unsupported.""" return + + +class XiaomiGatewayLight(LightEntity): + """Representation of a gateway device's light.""" + + def __init__(self, gateway_device, gateway_name, gateway_device_id): + """Initialize the XiaomiGatewayLight.""" + self._gateway = gateway_device + self._name = f"{gateway_name} Light" + self._gateway_device_id = gateway_device_id + self._unique_id = gateway_device_id + self._available = False + self._is_on = None + self._brightness_pct = 100 + self._rgb = (255, 255, 255) + self._hs = (0, 0) + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info of the gateway.""" + return { + "identifiers": {(DOMAIN, self._gateway_device_id)}, + } + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def available(self): + """Return true when state is known.""" + return self._available + + @property + def is_on(self): + """Return true if it is on.""" + return self._is_on + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int(255 * self._brightness_pct / 100) + + @property + def hs_color(self): + """Return the hs color value.""" + return self._hs + + @property + def supported_features(self): + """Return the supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + + def turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_HS_COLOR in kwargs: + rgb = color.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + else: + rgb = self._rgb + + if ATTR_BRIGHTNESS in kwargs: + brightness_pct = int(100 * kwargs[ATTR_BRIGHTNESS] / 255) + else: + brightness_pct = self._brightness_pct + + self._gateway.light.set_rgb(brightness_pct, rgb) + + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the light off.""" + self._gateway.light.set_rgb(0, self._rgb) + self.schedule_update_ha_state() + + async def async_update(self): + """Fetch state from the device.""" + try: + state_dict = await self.hass.async_add_executor_job( + self._gateway.light.rgb_status + ) + except GatewayException as ex: + if self._available: + self._available = False + _LOGGER.error( + "Got exception while fetching the gateway light state: %s", ex + ) + return + + self._available = True + self._is_on = state_dict["is_on"] + + if self._is_on: + self._brightness_pct = state_dict["brightness"] + self._rgb = state_dict["rgb"] + self._hs = color.color_RGB_to_hs(*self._rgb) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 924d6d8a23d..260a1cc8ae7 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -3,7 +3,13 @@ from dataclasses import dataclass import logging from miio import AirQualityMonitor, DeviceException # pylint: disable=import-error -from miio.gateway import DeviceType +from miio.gateway import ( + GATEWAY_MODEL_AC_V1, + GATEWAY_MODEL_AC_V2, + GATEWAY_MODEL_AC_V3, + DeviceType, + GatewayException, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -12,6 +18,7 @@ from homeassistant.const import ( CONF_NAME, CONF_TOKEN, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PRESSURE_HPA, @@ -78,9 +85,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Xiaomi sensor from a config entry.""" entities = [] - # Gateway sub devices if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: gateway = hass.data[DOMAIN][config_entry.entry_id] + # Gateway illuminance sensor + if gateway.model not in [ + GATEWAY_MODEL_AC_V1, + GATEWAY_MODEL_AC_V2, + GATEWAY_MODEL_AC_V3, + ]: + entities.append( + XiaomiGatewayIlluminanceSensor( + gateway, config_entry.title, config_entry.unique_id + ) + ) + # Gateway sub devices sub_devices = gateway.devices for sub_device in sub_devices.values(): sensor_variables = None @@ -250,3 +268,67 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice): def state(self): """Return the state of the sensor.""" return self._sub_device.status[self._data_key] + + +class XiaomiGatewayIlluminanceSensor(Entity): + """Representation of the gateway device's illuminance sensor.""" + + def __init__(self, gateway_device, gateway_name, gateway_device_id): + """Initialize the entity.""" + self._gateway = gateway_device + self._name = f"{gateway_name} Illuminance" + self._gateway_device_id = gateway_device_id + self._unique_id = f"{gateway_device_id}-illuminance" + self._available = False + self._state = None + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info of the gateway.""" + return { + "identifiers": {(DOMAIN, self._gateway_device_id)}, + } + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def available(self): + """Return true when state is known.""" + return self._available + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return "lux" + + @property + def device_class(self): + """Return the device class of this entity.""" + return DEVICE_CLASS_ILLUMINANCE + + @property + def state(self): + """Return the state of the device.""" + return self._state + + async def async_update(self): + """Fetch state from the device.""" + try: + self._state = await self.hass.async_add_executor_job( + self._gateway.get_illumination + ) + self._available = True + except GatewayException as ex: + if self._available: + self._available = False + _LOGGER.error( + "Got exception while fetching the gateway illuminance state: %s", ex + ) From 4d637e5f3032e77bf6402ceccd11308aa3ab035f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2020 06:54:15 -0500 Subject: [PATCH 525/862] Skip setup of dependencies if they are already setup (#39482) after_dependencies were checking hass.config.components to see if something was already setup. We did not do the same for dependencies which resulted in trying to set them up more then once. Noticed when `homeassistant.setup` was set to debug logging and many integrations were announcing waiting on http when it was already setup. --- homeassistant/setup.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 870b476d605..818e28479fb 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -71,27 +71,47 @@ async def _async_process_dependencies( hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration ) -> bool: """Ensure all dependencies are set up.""" - tasks = { + dependencies_tasks = { dep: hass.loop.create_task(async_setup_component(hass, dep, config)) for dep in integration.dependencies + if dep not in hass.config.components } + after_dependencies_tasks = dict() to_be_loaded = hass.data.get(DATA_SETUP_DONE, {}) for dep in integration.after_dependencies: - if dep in to_be_loaded and dep not in hass.config.components: - tasks[dep] = hass.loop.create_task(to_be_loaded[dep].wait()) + if ( + dep not in dependencies_tasks + and dep in to_be_loaded + and dep not in hass.config.components + ): + after_dependencies_tasks[dep] = hass.loop.create_task( + to_be_loaded[dep].wait() + ) - if not tasks: + if not dependencies_tasks and not after_dependencies_tasks: return True - _LOGGER.debug("Dependency %s will wait for %s", integration.domain, list(tasks)) + if dependencies_tasks: + _LOGGER.debug( + "Dependency %s will wait for dependencies %s", + integration.domain, + list(dependencies_tasks), + ) + if after_dependencies_tasks: + _LOGGER.debug( + "Dependency %s will wait for after dependencies %s", + integration.domain, + list(after_dependencies_tasks), + ) + async with hass.timeout.async_freeze(integration.domain): - results = await asyncio.gather(*tasks.values()) + results = await asyncio.gather( + *dependencies_tasks.values(), *after_dependencies_tasks.values() + ) failed = [ - domain - for idx, domain in enumerate(integration.dependencies) - if not results[idx] + domain for idx, domain in enumerate(dependencies_tasks) if not results[idx] ] if failed: From 7062838940330d866dc9f50d836ae37148dd5d15 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 31 Aug 2020 09:37:45 -0400 Subject: [PATCH 526/862] Add entity services to the Flo integration (#38287) Co-authored-by: Paulus Schoutsen --- homeassistant/components/flo/__init__.py | 8 +-- homeassistant/components/flo/binary_sensor.py | 6 +- homeassistant/components/flo/const.py | 6 +- homeassistant/components/flo/device.py | 18 +++++ homeassistant/components/flo/manifest.json | 2 +- homeassistant/components/flo/sensor.py | 6 +- homeassistant/components/flo/services.yaml | 32 +++++++++ homeassistant/components/flo/switch.py | 53 +++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flo/conftest.py | 37 ++++++++++ tests/components/flo/test_binary_sensor.py | 2 +- tests/components/flo/test_device.py | 6 +- tests/components/flo/test_init.py | 4 +- tests/components/flo/test_sensor.py | 4 +- tests/components/flo/test_services.py | 69 +++++++++++++++++++ tests/components/flo/test_switch.py | 2 +- 17 files changed, 237 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/flo/services.yaml create mode 100644 tests/components/flo/test_services.py diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 2233b4aa0c3..14d00aa000a 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CLIENT, DOMAIN from .device import FloDeviceDataUpdateCoordinator CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -30,10 +30,10 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up flo from a config entry.""" - hass.data[DOMAIN][entry.entry_id] = {} session = async_get_clientsession(hass) + hass.data[DOMAIN][entry.entry_id] = {} try: - hass.data[DOMAIN][entry.entry_id]["client"] = client = await async_get_api( + hass.data[DOMAIN][entry.entry_id][CLIENT] = client = await async_get_api( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session ) except RequestError as err: @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.debug("Flo user information with locations: %s", user_info) - hass.data[DOMAIN]["devices"] = devices = [ + hass.data[DOMAIN][entry.entry_id]["devices"] = devices = [ FloDeviceDataUpdateCoordinator(hass, client, location["id"], device["id"]) for location in user_info["locations"] for device in location["devices"] diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py index 09facae53ed..4af91a8ef77 100644 --- a/homeassistant/components/flo/binary_sensor.py +++ b/homeassistant/components/flo/binary_sensor.py @@ -11,12 +11,12 @@ from .const import DOMAIN as FLO_DOMAIN from .device import FloDeviceDataUpdateCoordinator from .entity import FloEntity -DEPENDENCIES = ["flo"] - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Flo sensors from config entry.""" - devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN]["devices"] + devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ + config_entry.entry_id + ]["devices"] entities = [FloPendingAlertsBinarySensor(device) for device in devices] async_add_entities(entities) diff --git a/homeassistant/components/flo/const.py b/homeassistant/components/flo/const.py index edeb469380b..94c1b8d4579 100644 --- a/homeassistant/components/flo/const.py +++ b/homeassistant/components/flo/const.py @@ -1,3 +1,7 @@ """Constants for the flo integration.""" - +CLIENT = "client" DOMAIN = "flo" +FLO_HOME = "home" +FLO_AWAY = "away" +FLO_SLEEP = "sleep" +FLO_MODES = [FLO_HOME, FLO_AWAY, FLO_SLEEP] diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index 1a01f45b641..824d62a9519 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -172,6 +172,24 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): """Return the target valve state for the device.""" return self._device_information["valve"]["target"] + async def async_set_mode_home(self): + """Set the Flo location to home mode.""" + await self.api_client.location.set_mode_home(self._flo_location_id) + + async def async_set_mode_away(self): + """Set the Flo location to away mode.""" + await self.api_client.location.set_mode_away(self._flo_location_id) + + async def async_set_mode_sleep(self, sleep_minutes, revert_to_mode): + """Set the Flo location to sleep mode.""" + await self.api_client.location.set_mode_sleep( + self._flo_location_id, sleep_minutes, revert_to_mode + ) + + async def async_run_health_test(self): + """Run a Flo device health test.""" + await self.api_client.device.run_health_test(self._flo_device_id) + async def _update_device(self, *_) -> None: """Update the device information from the API.""" self._device_information = await self.api_client.device.get_info( diff --git a/homeassistant/components/flo/manifest.json b/homeassistant/components/flo/manifest.json index cfcb6db1c5f..1d9ae596afb 100644 --- a/homeassistant/components/flo/manifest.json +++ b/homeassistant/components/flo/manifest.json @@ -3,7 +3,7 @@ "name": "Flo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flo", - "requirements": ["aioflo==0.4.0"], + "requirements": ["aioflo==0.4.1"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index cac259f475f..2feeb3702a6 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -15,8 +15,6 @@ from .const import DOMAIN as FLO_DOMAIN from .device import FloDeviceDataUpdateCoordinator from .entity import FloEntity -DEPENDENCIES = ["flo"] - WATER_ICON = "mdi:water" GAUGE_ICON = "mdi:gauge" NAME_DAILY_USAGE = "Today's Water Usage" @@ -28,7 +26,9 @@ NAME_WATER_PRESSURE = "Water Pressure" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Flo sensors from config entry.""" - devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN]["devices"] + devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ + config_entry.entry_id + ]["devices"] entities = [] entities.extend([FloDailyUsageSensor(device) for device in devices]) entities.extend([FloSystemModeSensor(device) for device in devices]) diff --git a/homeassistant/components/flo/services.yaml b/homeassistant/components/flo/services.yaml new file mode 100644 index 00000000000..b5797020ac0 --- /dev/null +++ b/homeassistant/components/flo/services.yaml @@ -0,0 +1,32 @@ +# Describes the format for available Flo services + +set_sleep_mode: + description: Set the location into sleep mode. + fields: + entity_id: + description: Flo switch entity id + example: "switch.shutoff_valve" + sleep_minutes: + description: The time to sleep in minutes. + example: 120 + revert_to_mode: + description: The mode to revert to after sleep_minutes has elapsed. + example: "home" +set_away_mode: + description: Set the location into away mode. + fields: + entity_id: + description: Flo switch entity id + example: "switch.shutoff_valve" +set_home_mode: + description: Set the location into home mode. + fields: + entity_id: + description: Flo switch entity id + example: "switch.shutoff_valve" +run_health_test: + description: Have the Flo device run a health test. + fields: + entity_id: + description: Flo switch entity id + example: "switch.shutoff_valve" diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index cabf8135ad9..91f3fdf54e4 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -2,19 +2,54 @@ from typing import List +from aioflo.location import SLEEP_MINUTE_OPTIONS, SYSTEM_MODE_HOME, SYSTEM_REVERT_MODES +import voluptuous as vol + from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback +from homeassistant.helpers import entity_platform from .const import DOMAIN as FLO_DOMAIN from .device import FloDeviceDataUpdateCoordinator from .entity import FloEntity +ATTR_REVERT_TO_MODE = "revert_to_mode" +ATTR_SLEEP_MINUTES = "sleep_minutes" +SERVICE_SET_SLEEP_MODE = "set_sleep_mode" +SERVICE_SET_AWAY_MODE = "set_away_mode" +SERVICE_SET_HOME_MODE = "set_home_mode" +SERVICE_RUN_HEALTH_TEST = "run_health_test" + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Flo switches from config entry.""" - devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN]["devices"] + devices: List[FloDeviceDataUpdateCoordinator] = hass.data[FLO_DOMAIN][ + config_entry.entry_id + ]["devices"] async_add_entities([FloSwitch(device) for device in devices]) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_AWAY_MODE, {}, "async_set_mode_away" + ) + platform.async_register_entity_service( + SERVICE_SET_HOME_MODE, {}, "async_set_mode_home" + ) + platform.async_register_entity_service( + SERVICE_RUN_HEALTH_TEST, {}, "async_run_health_test" + ) + platform.async_register_entity_service( + SERVICE_SET_SLEEP_MODE, + { + vol.Required(ATTR_SLEEP_MINUTES, default=120): vol.In(SLEEP_MINUTE_OPTIONS), + vol.Required(ATTR_REVERT_TO_MODE, default=SYSTEM_MODE_HOME): vol.In( + SYSTEM_REVERT_MODES + ), + }, + "async_set_mode_sleep", + ) + class FloSwitch(FloEntity, SwitchEntity): """Switch class for the Flo by Moen valve.""" @@ -57,3 +92,19 @@ class FloSwitch(FloEntity, SwitchEntity): async def async_added_to_hass(self): """When entity is added to hass.""" self.async_on_remove(self._device.async_add_listener(self.async_update_state)) + + async def async_set_mode_home(self): + """Set the Flo location to home mode.""" + await self._device.async_set_mode_home() + + async def async_set_mode_away(self): + """Set the Flo location to away mode.""" + await self._device.async_set_mode_away() + + async def async_set_mode_sleep(self, sleep_minutes, revert_to_mode): + """Set the Flo location to sleep mode.""" + await self._device.async_set_mode_sleep(sleep_minutes, revert_to_mode) + + async def async_run_health_test(self): + """Run a Flo device health test.""" + await self._device.async_run_health_test() diff --git a/requirements_all.txt b/requirements_all.txt index 07ba08104f4..e7c885b3ebb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,7 @@ aioeafm==0.1.2 aioesphomeapi==2.6.1 # homeassistant.components.flo -aioflo==0.4.0 +aioflo==0.4.1 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f63650066a..98cd6b9105d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -91,7 +91,7 @@ aioeafm==0.1.2 aioesphomeapi==2.6.1 # homeassistant.components.flo -aioflo==0.4.0 +aioflo==0.4.1 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py index 982bdbdec0d..3a835cb0547 100644 --- a/tests/components/flo/conftest.py +++ b/tests/components/flo/conftest.py @@ -95,3 +95,40 @@ def aioclient_mock_fixture(aioclient_mock): headers={"Content-Type": "application/json"}, json={"valve": {"target": "closed"}}, ) + # Mocks the health test call for flo. + aioclient_mock.post( + "https://api-gw.meetflo.com/api/v2/devices/98765/healthTest/run", + text=load_fixture("flo/user_info_expand_locations_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + ) + # Mocks the health test call for flo. + aioclient_mock.post( + "https://api-gw.meetflo.com/api/v2/locations/mmnnoopp/systemMode", + text=load_fixture("flo/user_info_expand_locations_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + json={"systemMode": {"target": "home"}}, + ) + # Mocks the health test call for flo. + aioclient_mock.post( + "https://api-gw.meetflo.com/api/v2/locations/mmnnoopp/systemMode", + text=load_fixture("flo/user_info_expand_locations_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + json={"systemMode": {"target": "away"}}, + ) + # Mocks the health test call for flo. + aioclient_mock.post( + "https://api-gw.meetflo.com/api/v2/locations/mmnnoopp/systemMode", + text=load_fixture("flo/user_info_expand_locations_response.json"), + status=200, + headers={"Content-Type": "application/json"}, + json={ + "systemMode": { + "target": "sleep", + "revertMinutes": 120, + "revertMode": "home", + } + }, + ) diff --git a/tests/components/flo/test_binary_sensor.py b/tests/components/flo/test_binary_sensor.py index 9f727c5a10b..64b8f787a85 100644 --- a/tests/components/flo/test_binary_sensor.py +++ b/tests/components/flo/test_binary_sensor.py @@ -19,7 +19,7 @@ async def test_binary_sensors(hass, config_entry, aioclient_mock_fixture): ) await hass.async_block_till_done() - assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 # we should have 6 entities for the device state = hass.states.get("binary_sensor.pending_system_alerts") diff --git a/tests/components/flo/test_device.py b/tests/components/flo/test_device.py index db5c8cd5c9e..63e81a16fb4 100644 --- a/tests/components/flo/test_device.py +++ b/tests/components/flo/test_device.py @@ -19,9 +19,11 @@ async def test_device(hass, config_entry, aioclient_mock_fixture, aioclient_mock hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} ) await hass.async_block_till_done() - assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 - device: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN]["devices"][0] + device: FloDeviceDataUpdateCoordinator = hass.data[FLO_DOMAIN][ + config_entry.entry_id + ]["devices"][0] assert device.api_client is not None assert device.available assert device.consumption_today == 3.674 diff --git a/tests/components/flo/test_init.py b/tests/components/flo/test_init.py index c0eaf535f35..9061477da47 100644 --- a/tests/components/flo/test_init.py +++ b/tests/components/flo/test_init.py @@ -13,4 +13,6 @@ async def test_setup_entry(hass, config_entry, aioclient_mock_fixture): hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} ) await hass.async_block_till_done() - assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 + + assert await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index ab5132bd34e..309dfc11266 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -14,7 +14,7 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture): ) await hass.async_block_till_done() - assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 # we should have 5 entities for the device assert hass.states.get("sensor.current_system_mode").state == "home" @@ -34,7 +34,7 @@ async def test_manual_update_entity( ) await hass.async_block_till_done() - assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 await async_setup_component(hass, "homeassistant", {}) diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py new file mode 100644 index 00000000000..270279e4a9d --- /dev/null +++ b/tests/components/flo/test_services.py @@ -0,0 +1,69 @@ +"""Test the services for the Flo by Moen integration.""" +from homeassistant.components.flo.const import DOMAIN as FLO_DOMAIN +from homeassistant.components.flo.switch import ( + ATTR_REVERT_TO_MODE, + ATTR_SLEEP_MINUTES, + SERVICE_RUN_HEALTH_TEST, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_HOME_MODE, + SERVICE_SET_SLEEP_MODE, + SYSTEM_MODE_HOME, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from .common import TEST_PASSWORD, TEST_USER_ID + +SWITCH_ENTITY_ID = "switch.shutoff_valve" + + +async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mock): + """Test Flo services.""" + config_entry.add_to_hass(hass) + assert await async_setup_component( + hass, FLO_DOMAIN, {CONF_USERNAME: TEST_USER_ID, CONF_PASSWORD: TEST_PASSWORD} + ) + await hass.async_block_till_done() + + assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 + assert aioclient_mock.call_count == 4 + + await hass.services.async_call( + FLO_DOMAIN, + SERVICE_RUN_HEALTH_TEST, + {ATTR_ENTITY_ID: SWITCH_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + assert aioclient_mock.call_count == 5 + + await hass.services.async_call( + FLO_DOMAIN, + SERVICE_SET_AWAY_MODE, + {ATTR_ENTITY_ID: SWITCH_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + assert aioclient_mock.call_count == 6 + + await hass.services.async_call( + FLO_DOMAIN, + SERVICE_SET_HOME_MODE, + {ATTR_ENTITY_ID: SWITCH_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + assert aioclient_mock.call_count == 7 + + await hass.services.async_call( + FLO_DOMAIN, + SERVICE_SET_SLEEP_MODE, + { + ATTR_ENTITY_ID: SWITCH_ENTITY_ID, + ATTR_REVERT_TO_MODE: SYSTEM_MODE_HOME, + ATTR_SLEEP_MINUTES: 120, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert aioclient_mock.call_count == 8 diff --git a/tests/components/flo/test_switch.py b/tests/components/flo/test_switch.py index f821e5f57d9..25a64433a29 100644 --- a/tests/components/flo/test_switch.py +++ b/tests/components/flo/test_switch.py @@ -15,7 +15,7 @@ async def test_valve_switches(hass, config_entry, aioclient_mock_fixture): ) await hass.async_block_till_done() - assert len(hass.data[FLO_DOMAIN]["devices"]) == 1 + assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 1 entity_id = "switch.shutoff_valve" assert hass.states.get(entity_id).state == STATE_ON From 3ab666343471561fa3a9a23140fde58d95291b35 Mon Sep 17 00:00:00 2001 From: Stefan Lehmann Date: Mon, 31 Aug 2020 15:53:16 +0200 Subject: [PATCH 527/862] Fix ADS component by bumping pyads version to 3.2.2 (#39502) --- homeassistant/components/ads/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index 5da1b79b424..cee2419b4fe 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -2,6 +2,6 @@ "domain": "ads", "name": "ADS", "documentation": "https://www.home-assistant.io/integrations/ads", - "requirements": ["pyads==3.2.1"], + "requirements": ["pyads==3.2.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index e7c885b3ebb..56c53235f58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1207,7 +1207,7 @@ py_nextbusnext==0.1.4 # py_noaa==0.3.0 # homeassistant.components.ads -pyads==3.2.1 +pyads==3.2.2 # homeassistant.components.hisense_aehw4a1 pyaehw4a1==0.3.9 From 45a927ffb27de1ab8171c5bd18fc2b1b2e666bc2 Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Mon, 31 Aug 2020 22:40:56 +0800 Subject: [PATCH 528/862] Add config flow to yeelight (#37191) --- CODEOWNERS | 2 +- .../components/discovery/__init__.py | 2 +- homeassistant/components/yeelight/__init__.py | 330 ++++++++++++++---- .../components/yeelight/binary_sensor.py | 34 +- .../components/yeelight/config_flow.py | 194 ++++++++++ homeassistant/components/yeelight/light.py | 51 ++- .../components/yeelight/manifest.json | 14 +- .../components/yeelight/strings.json | 39 +++ .../components/yeelight/translations/en.json | 39 +++ homeassistant/generated/config_flows.py | 1 + tests/components/yeelight/__init__.py | 17 +- .../components/yeelight/test_binary_sensor.py | 4 +- tests/components/yeelight/test_config_flow.py | 261 ++++++++++++++ tests/components/yeelight/test_init.py | 69 ++++ tests/components/yeelight/test_light.py | 192 +++++----- 15 files changed, 1044 insertions(+), 205 deletions(-) create mode 100644 homeassistant/components/yeelight/config_flow.py create mode 100644 homeassistant/components/yeelight/strings.json create mode 100644 homeassistant/components/yeelight/translations/en.json create mode 100644 tests/components/yeelight/test_config_flow.py create mode 100644 tests/components/yeelight/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index ecf7745e595..6a2955f3c13 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -490,7 +490,7 @@ homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth homeassistant/components/yandex_transport/* @rishatik92 @devbis -homeassistant/components/yeelight/* @rytilahti @zewelor +homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf homeassistant/components/yi/* @bachya diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 2879d4bfbec..bf3a50c837e 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -64,7 +64,6 @@ SERVICE_HANDLERS = { SERVICE_KONNECTED: ("konnected", None), SERVICE_OCTOPRINT: ("octoprint", None), SERVICE_FREEBOX: ("freebox", None), - SERVICE_YEELIGHT: ("yeelight", None), "yamaha": ("media_player", "yamaha"), "frontier_silicon": ("media_player", "frontier_silicon"), "openhome": ("media_player", "openhome"), @@ -93,6 +92,7 @@ MIGRATED_SERVICE_HANDLERS = [ SERVICE_WEMO, SERVICE_XIAOMI_GW, "volumio", + SERVICE_YEELIGHT, ] DEFAULT_ENABLED = ( diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index b0413599fe3..e463e5dad3f 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -1,27 +1,26 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" - +import asyncio from datetime import timedelta import logging from typing import Optional import voluptuous as vol -from yeelight import Bulb, BulbException +from yeelight import Bulb, BulbException, discover_bulbs -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.discovery import SERVICE_YEELIGHT -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICES, - CONF_HOST, + CONF_ID, + CONF_IP_ADDRESS, CONF_NAME, CONF_SCAN_INTERVAL, ) -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import dispatcher_connect, dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -32,6 +31,9 @@ DEVICE_INITIALIZED = f"{DOMAIN}_device_initialized" DEFAULT_NAME = "Yeelight" DEFAULT_TRANSITION = 350 +DEFAULT_MODE_MUSIC = False +DEFAULT_SAVE_ON_CHANGE = False +DEFAULT_NIGHTLIGHT_SWITCH = False CONF_MODEL = "model" CONF_TRANSITION = "transition" @@ -40,6 +42,14 @@ CONF_MODE_MUSIC = "use_music_mode" CONF_FLOW_PARAMS = "flow_params" CONF_CUSTOM_EFFECTS = "custom_effects" CONF_NIGHTLIGHT_SWITCH_TYPE = "nightlight_switch_type" +CONF_NIGHTLIGHT_SWITCH = "nightlight_switch" +CONF_DEVICE = "device" + +DATA_CONFIG_ENTRIES = "config_entries" +DATA_CUSTOM_EFFECTS = "custom_effects" +DATA_SCAN_INTERVAL = "scan_interval" +DATA_DEVICE = "device" +DATA_UNSUB_UPDATE_LISTENER = "unsub_update_listener" ATTR_COUNT = "count" ATTR_ACTION = "action" @@ -55,6 +65,7 @@ ACTIVE_COLOR_FLOWING = "1" NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" SCAN_INTERVAL = timedelta(seconds=30) +DISCOVERY_INTERVAL = timedelta(seconds=60) YEELIGHT_RGB_TRANSITION = "RGBTransition" YEELIGHT_HSV_TRANSACTION = "HSVTransition" @@ -139,73 +150,221 @@ UPDATE_REQUEST_PROPERTIES = [ "active_mode", ] +PLATFORMS = ["binary_sensor", "light"] -def setup(hass, config): + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the Yeelight bulbs.""" conf = config.get(DOMAIN, {}) - yeelight_data = hass.data[DATA_YEELIGHT] = {} + hass.data[DOMAIN] = { + DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}), + DATA_CONFIG_ENTRIES: {}, + DATA_SCAN_INTERVAL: conf.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), + } - def device_discovered(_, info): - _LOGGER.debug("Adding autodetected %s", info["hostname"]) - - name = "yeelight_{}_{}".format(info["device_type"], info["properties"]["mac"]) - - device_config = DEVICE_SCHEMA({CONF_NAME: name}) - - _setup_device(hass, config, info[CONF_HOST], device_config) - - discovery.listen(hass, SERVICE_YEELIGHT, device_discovered) - - def update(_): - for device in list(yeelight_data.values()): - device.update() - - track_time_interval(hass, update, conf.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)) - - def load_platforms(ipaddr): - platform_config = hass.data[DATA_YEELIGHT][ipaddr].config.copy() - platform_config[CONF_HOST] = ipaddr - platform_config[CONF_CUSTOM_EFFECTS] = config.get(DOMAIN, {}).get( - CONF_CUSTOM_EFFECTS, {} + # Import manually configured devices + for ipaddr, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items(): + _LOGGER.debug("Importing configured %s", ipaddr) + entry_config = { + CONF_IP_ADDRESS: ipaddr, + **device_config, + } + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=entry_config, + ), ) - load_platform(hass, LIGHT_DOMAIN, DOMAIN, platform_config, config) - load_platform(hass, BINARY_SENSOR_DOMAIN, DOMAIN, platform_config, config) - - dispatcher_connect(hass, DEVICE_INITIALIZED, load_platforms) - - if DOMAIN in config: - for ipaddr, device_config in conf[CONF_DEVICES].items(): - _LOGGER.debug("Adding configured %s", device_config[CONF_NAME]) - _setup_device(hass, config, ipaddr, device_config) return True -def _setup_device(hass, _, ipaddr, device_config): - devices = hass.data[DATA_YEELIGHT] +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Yeelight from a config entry.""" - if ipaddr in devices: - return + async def _initialize(ipaddr: str) -> None: + device = await _async_setup_device(hass, ipaddr, entry.options) + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) - device = YeelightDevice(hass, ipaddr, device_config) + # Move options from data for imported entries + # Initialize options with default values for other entries + if not entry.options: + hass.config_entries.async_update_entry( + entry, + data={ + CONF_IP_ADDRESS: entry.data.get(CONF_IP_ADDRESS), + CONF_ID: entry.data.get(CONF_ID), + }, + options={ + CONF_NAME: entry.data.get(CONF_NAME, ""), + CONF_MODEL: entry.data.get(CONF_MODEL, ""), + CONF_TRANSITION: entry.data.get(CONF_TRANSITION, DEFAULT_TRANSITION), + CONF_MODE_MUSIC: entry.data.get(CONF_MODE_MUSIC, DEFAULT_MODE_MUSIC), + CONF_SAVE_ON_CHANGE: entry.data.get( + CONF_SAVE_ON_CHANGE, DEFAULT_SAVE_ON_CHANGE + ), + CONF_NIGHTLIGHT_SWITCH: entry.data.get( + CONF_NIGHTLIGHT_SWITCH, DEFAULT_NIGHTLIGHT_SWITCH + ), + }, + ) - devices[ipaddr] = device - hass.add_job(device.setup) + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = { + DATA_UNSUB_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener) + } + + if entry.data.get(CONF_IP_ADDRESS): + # manually added device + await _initialize(entry.data[CONF_IP_ADDRESS]) + else: + # discovery + scanner = YeelightScanner.async_get(hass) + scanner.async_register_callback(entry.data[CONF_ID], _initialize) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES].pop(entry.entry_id) + data[DATA_UNSUB_UPDATE_LISTENER]() + data[DATA_DEVICE].async_unload() + if entry.data[CONF_ID]: + # discovery + scanner = YeelightScanner.async_get(hass) + scanner.async_unregister_callback(entry.data[CONF_ID]) + + return unload_ok + + +async def _async_setup_device( + hass: HomeAssistant, + ipaddr: str, + config: dict, +) -> None: + # Set up device + bulb = Bulb(ipaddr, model=config.get(CONF_MODEL) or None) + capabilities = await hass.async_add_executor_job(bulb.get_capabilities) + if capabilities is None: # timeout + _LOGGER.error("Failed to get capabilities from %s", ipaddr) + raise ConfigEntryNotReady + device = YeelightDevice(hass, ipaddr, config, bulb) + await hass.async_add_executor_job(device.update) + await device.async_setup() + return device + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +class YeelightScanner: + """Scan for Yeelight devices.""" + + _scanner = None + + @classmethod + @callback + def async_get(cls, hass: HomeAssistant): + """Get scanner instance.""" + if cls._scanner is None: + cls._scanner = cls(hass) + return cls._scanner + + def __init__(self, hass: HomeAssistant): + """Initialize class.""" + self._hass = hass + self._seen = {} + self._callbacks = {} + self._scan_task = None + + async def _async_scan(self): + _LOGGER.debug("Yeelight scanning") + # Run 3 times as packets can get lost + for _ in range(3): + devices = await self._hass.async_add_executor_job(discover_bulbs) + for device in devices: + unique_id = device["capabilities"]["id"] + if unique_id in self._seen: + continue + ipaddr = device["ip"] + self._seen[unique_id] = ipaddr + _LOGGER.debug("Yeelight discovered at %s", ipaddr) + if unique_id in self._callbacks: + self._hass.async_create_task(self._callbacks[unique_id](ipaddr)) + self._callbacks.pop(unique_id) + if len(self._callbacks) == 0: + self._async_stop_scan() + + await asyncio.sleep(SCAN_INTERVAL.seconds) + self._scan_task = self._hass.loop.create_task(self._async_scan()) + + @callback + def _async_start_scan(self): + """Start scanning for Yeelight devices.""" + _LOGGER.debug("Start scanning") + # Use loop directly to avoid home assistant track this task + self._scan_task = self._hass.loop.create_task(self._async_scan()) + + @callback + def _async_stop_scan(self): + """Stop scanning.""" + _LOGGER.debug("Stop scanning") + if self._scan_task is not None: + self._scan_task.cancel() + self._scan_task = None + + @callback + def async_register_callback(self, unique_id, callback_func): + """Register callback function.""" + ipaddr = self._seen.get(unique_id) + if ipaddr is not None: + self._hass.async_add_job(callback_func(ipaddr)) + else: + self._callbacks[unique_id] = callback_func + if len(self._callbacks) == 1: + self._async_start_scan() + + @callback + def async_unregister_callback(self, unique_id): + """Unregister callback function.""" + if unique_id not in self._callbacks: + return + self._callbacks.pop(unique_id) + if len(self._callbacks) == 0: + self._async_stop_scan() class YeelightDevice: """Represents single Yeelight device.""" - def __init__(self, hass, ipaddr, config): + def __init__(self, hass, ipaddr, config, bulb): """Initialize device.""" self._hass = hass self._config = config self._ipaddr = ipaddr - self._name = config.get(CONF_NAME) - self._bulb_device = Bulb(self.ipaddr, model=config.get(CONF_MODEL)) + unique_id = bulb.capabilities.get("id") + self._name = config.get(CONF_NAME) or f"yeelight_{bulb.model}_{unique_id}" + self._bulb_device = bulb self._device_type = None self._available = False - self._initialized = False + self._remove_time_tracker = None @property def bulb(self): @@ -237,6 +396,11 @@ class YeelightDevice: """Return configured/autodetected device model.""" return self._bulb_device.model + @property + def fw_version(self): + """Return the firmware version.""" + return self._bulb_device.capabilities.get("fw_ver") + @property def is_nightlight_supported(self) -> bool: """ @@ -319,8 +483,6 @@ class YeelightDevice: try: self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True - if not self._initialized: - self._initialize_device() except BulbException as ex: if self._available: # just inform once _LOGGER.error( @@ -348,16 +510,56 @@ class YeelightDevice: ex, ) - def _initialize_device(self): - self._get_capabilities() - self._initialized = True - dispatcher_send(self._hass, DEVICE_INITIALIZED, self.ipaddr) - def update(self): """Update device properties and send data updated signal.""" self._update_properties() dispatcher_send(self._hass, DATA_UPDATED.format(self._ipaddr)) - def setup(self): - """Fetch initial device properties.""" - self._update_properties() + async def async_setup(self): + """Set up the device.""" + + async def _async_update(_): + await self._hass.async_add_executor_job(self.update) + + await _async_update(None) + self._remove_time_tracker = async_track_time_interval( + self._hass, _async_update, self._hass.data[DOMAIN][DATA_SCAN_INTERVAL] + ) + + @callback + def async_unload(self): + """Unload the device.""" + self._remove_time_tracker() + + +class YeelightEntity(Entity): + """Represents single Yeelight entity.""" + + def __init__(self, device: YeelightDevice): + """Initialize the entity.""" + self._device = device + + @property + def device_info(self) -> dict: + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self._device.unique_id)}, + "name": self._device.name, + "manufacturer": "Yeelight", + "model": self._device.model, + "sw_version": self._device.fw_version, + } + + @property + def available(self) -> bool: + """Return if bulb is available.""" + return self._device.available + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + def update(self) -> None: + """Update the entity.""" + self._device.update() diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 1696ca9bcb2..ae811cd91d3 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -3,32 +3,28 @@ import logging from typing import Optional from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_UPDATED, DATA_YEELIGHT +from . import DATA_CONFIG_ENTRIES, DATA_DEVICE, DATA_UPDATED, DOMAIN, YeelightEntity _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Yeelight sensors.""" - if not discovery_info: - return - - device = hass.data[DATA_YEELIGHT][discovery_info["host"]] - +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up Yeelight from a config entry.""" + device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE] if device.is_nightlight_supported: _LOGGER.debug("Adding nightlight mode sensor for %s", device.name) - add_entities([YeelightNightlightModeSensor(device)]) + async_add_entities([YeelightNightlightModeSensor(device)]) -class YeelightNightlightModeSensor(BinarySensorEntity): +class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity): """Representation of a Yeelight nightlight mode sensor.""" - def __init__(self, device): - """Initialize nightlight mode sensor.""" - self._device = device - async def async_added_to_hass(self): """Handle entity which will be added.""" self.async_on_remove( @@ -49,16 +45,6 @@ class YeelightNightlightModeSensor(BinarySensorEntity): return None - @property - def available(self) -> bool: - """Return if bulb is available.""" - return self._device.available - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py new file mode 100644 index 00000000000..656680c9c8b --- /dev/null +++ b/homeassistant/components/yeelight/config_flow.py @@ -0,0 +1,194 @@ +"""Config flow for Yeelight integration.""" +import logging + +import voluptuous as vol +import yeelight + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_ID, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from . import ( + CONF_DEVICE, + CONF_MODE_MUSIC, + CONF_MODEL, + CONF_NIGHTLIGHT_SWITCH, + CONF_NIGHTLIGHT_SWITCH_TYPE, + CONF_SAVE_ON_CHANGE, + CONF_TRANSITION, + NIGHTLIGHT_SWITCH_TYPE_LIGHT, +) +from . import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Yeelight.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Return the options flow.""" + return OptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the config flow.""" + self._capabilities = None + self._discovered_devices = {} + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if user_input.get(CONF_IP_ADDRESS): + try: + await self._async_try_connect(user_input[CONF_IP_ADDRESS]) + return self.async_create_entry( + title=self._async_default_name(), + data=user_input, + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + else: + return await self.async_step_pick_device() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Optional(CONF_IP_ADDRESS): str}), + errors=errors, + ) + + async def async_step_pick_device(self, user_input=None): + """Handle the step to pick discovered device.""" + if user_input is not None: + unique_id = user_input[CONF_DEVICE] + self._capabilities = self._discovered_devices[unique_id] + return self.async_create_entry( + title=self._async_default_name(), + data={CONF_ID: unique_id}, + ) + + configured_devices = { + entry.data[CONF_ID] + for entry in self._async_current_entries() + if entry.data[CONF_ID] + } + devices_name = {} + # Run 3 times as packets can get lost + for _ in range(3): + devices = await self.hass.async_add_executor_job(yeelight.discover_bulbs) + for device in devices: + capabilities = device["capabilities"] + unique_id = capabilities["id"] + if unique_id in configured_devices: + continue # ignore configured devices + model = capabilities["model"] + ipaddr = device["ip"] + name = f"{ipaddr} {model} {unique_id}" + self._discovered_devices[unique_id] = capabilities + devices_name[unique_id] = name + + # Check if there is at least one device + if not devices_name: + return self.async_abort(reason="no_devices_found") + return self.async_show_form( + step_id="pick_device", + data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), + ) + + async def async_step_import(self, user_input=None): + """Handle import step.""" + ipaddr = user_input[CONF_IP_ADDRESS] + try: + await self._async_try_connect(ipaddr) + except CannotConnect: + _LOGGER.error("Failed to import %s: cannot connect", ipaddr) + return self.async_abort(reason="cannot_connect") + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + if CONF_NIGHTLIGHT_SWITCH_TYPE in user_input: + user_input[CONF_NIGHTLIGHT_SWITCH] = ( + user_input.pop(CONF_NIGHTLIGHT_SWITCH_TYPE) + == NIGHTLIGHT_SWITCH_TYPE_LIGHT + ) + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + + async def _async_try_connect(self, ipaddr): + """Set up with options.""" + bulb = yeelight.Bulb(ipaddr) + try: + capabilities = await self.hass.async_add_executor_job(bulb.get_capabilities) + if capabilities is None: # timeout + _LOGGER.error("Failed to get capabilities from %s: timeout", ipaddr) + raise CannotConnect + except OSError as err: + _LOGGER.error("Failed to get capabilities from %s: %s", ipaddr, err) + raise CannotConnect from err + _LOGGER.debug("Get capabilities: %s", capabilities) + self._capabilities = capabilities + await self.async_set_unique_id(capabilities["id"]) + self._abort_if_unique_id_configured() + + @callback + def _async_default_name(self): + model = self._capabilities["model"] + unique_id = self._capabilities["id"] + return f"yeelight_{model}_{unique_id}" + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Yeelight.""" + + def __init__(self, config_entry): + """Initialize the option flow.""" + self._config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + # keep the name from imported entries + options = { + CONF_NAME: self._config_entry.options.get(CONF_NAME), + **user_input, + } + return self.async_create_entry(title="", data=options) + + options = self._config_entry.options + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional(CONF_MODEL, default=options[CONF_MODEL]): str, + vol.Required( + CONF_TRANSITION, + default=options[CONF_TRANSITION], + ): cv.positive_int, + vol.Required( + CONF_MODE_MUSIC, default=options[CONF_MODE_MUSIC] + ): bool, + vol.Required( + CONF_SAVE_ON_CHANGE, + default=options[CONF_SAVE_ON_CHANGE], + ): bool, + vol.Required( + CONF_NIGHTLIGHT_SWITCH, + default=options[CONF_NIGHTLIGHT_SWITCH], + ): bool, + } + ), + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class AlreadyConfigured(exceptions.HomeAssistantError): + """Indicate the ip address is already configured.""" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 793af380db9..cc580d60700 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,4 +1,5 @@ """Light platform support for yeelight.""" +from functools import partial import logging from typing import Optional @@ -32,8 +33,9 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, LightEntity, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_HOST, CONF_NAME -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service import extract_entity_ids @@ -48,18 +50,20 @@ from . import ( ATTR_ACTION, ATTR_COUNT, ATTR_TRANSITIONS, - CONF_CUSTOM_EFFECTS, CONF_FLOW_PARAMS, CONF_MODE_MUSIC, - CONF_NIGHTLIGHT_SWITCH_TYPE, + CONF_NIGHTLIGHT_SWITCH, CONF_SAVE_ON_CHANGE, CONF_TRANSITION, + DATA_CONFIG_ENTRIES, + DATA_CUSTOM_EFFECTS, + DATA_DEVICE, DATA_UPDATED, DATA_YEELIGHT, DOMAIN, - NIGHTLIGHT_SWITCH_TYPE_LIGHT, YEELIGHT_FLOW_TRANSITION_SCHEMA, YEELIGHT_SERVICE_SCHEMA, + YeelightEntity, ) _LOGGER = logging.getLogger(__name__) @@ -236,22 +240,20 @@ def _cmd(func): return _wrap -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Yeelight bulbs.""" - - if not discovery_info: - return +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up Yeelight from a config entry.""" if PLATFORM_DATA_KEY not in hass.data: hass.data[PLATFORM_DATA_KEY] = [] - device = hass.data[DATA_YEELIGHT][discovery_info[CONF_HOST]] + custom_effects = _parse_custom_effects(hass.data[DOMAIN][DATA_CUSTOM_EFFECTS]) + + device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE] _LOGGER.debug("Adding %s", device.name) - custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS]) - nl_switch_light = ( - discovery_info.get(CONF_NIGHTLIGHT_SWITCH_TYPE) == NIGHTLIGHT_SWITCH_TYPE_LIGHT - ) + nl_switch_light = device.config.get(CONF_NIGHTLIGHT_SWITCH) lights = [] @@ -290,8 +292,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) hass.data[PLATFORM_DATA_KEY] += lights - add_entities(lights, True) - setup_services(hass) + async_add_entities(lights, True) + await hass.async_add_executor_job(partial(setup_services, hass)) def setup_services(hass): @@ -406,13 +408,14 @@ def setup_services(hass): ) -class YeelightGenericLight(LightEntity): +class YeelightGenericLight(YeelightEntity, LightEntity): """Representation of a Yeelight generic light.""" def __init__(self, device, custom_effects=None): """Initialize the Yeelight light.""" + super().__init__(device) + self.config = device.config - self._device = device self._brightness = None self._color_temp = None @@ -444,22 +447,12 @@ class YeelightGenericLight(LightEntity): ) ) - @property - def should_poll(self): - """No polling needed.""" - return False - @property def unique_id(self) -> Optional[str]: """Return a unique ID.""" return self.device.unique_id - @property - def available(self) -> bool: - """Return if bulb is available.""" - return self.device.available - @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 32ccf1c117e..92baced836a 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,13 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.5.2"], - "after_dependencies": ["discovery"], - "codeowners": ["@rytilahti", "@zewelor"] -} + "requirements": [ + "yeelight==0.5.2" + ], + "codeowners": [ + "@rytilahti", + "@zewelor", + "@shenxn" + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/strings.json b/homeassistant/components/yeelight/strings.json new file mode 100644 index 00000000000..cc52b83f080 --- /dev/null +++ b/homeassistant/components/yeelight/strings.json @@ -0,0 +1,39 @@ +{ + "title": "Yeelight", + "config": { + "step": { + "user": { + "description": "If you leave IP address empty, discovery will be used to find devices.", + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + } + }, + "pick_device": { + "data": { + "device": "Device" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + }, + "options": { + "step": { + "init": { + "description": "If you leave model empty, it will be automatically detected.", + "data": { + "model": "Model (Optional)", + "transition": "Transition Time (ms)", + "use_music_mode": "Enable Music Mode", + "save_on_change": "Save Status On Change", + "nightlight_switch": "Use Nightlight Switch" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/en.json b/homeassistant/components/yeelight/translations/en.json new file mode 100644 index 00000000000..68adafdf376 --- /dev/null +++ b/homeassistant/components/yeelight/translations/en.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "pick_device": { + "data": { + "device": "Device" + } + }, + "user": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + }, + "description": "If you leave IP address empty, discovery will be used to find devices." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Model (Optional)", + "nightlight_switch": "Use Nightlight Switch", + "save_on_change": "Save Status On Change", + "transition": "Transition Time (ms)", + "use_music_mode": "Enable Music Mode" + }, + "description": "If you leave model empty, it will be automatically detected." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 62877b614b1..05ce927c773 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -208,6 +208,7 @@ FLOWS = [ "wolflink", "xiaomi_aqara", "xiaomi_miio", + "yeelight", "zerproc", "zha", "zwave" diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 7f1f7d7d236..a2f5b935947 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -9,9 +9,9 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_DEVICES, CONF_NAME +from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME -from tests.async_mock import MagicMock +from tests.async_mock import MagicMock, patch IP_ADDRESS = "192.168.1.239" MODEL = "color" @@ -70,6 +70,10 @@ YAML_CONFIGURATION = { } } +CONFIG_ENTRY_DATA = { + CONF_ID: ID, +} + def _mocked_bulb(cannot_connect=False): bulb = MagicMock() @@ -85,3 +89,12 @@ def _mocked_bulb(cannot_connect=False): bulb.music_mode = False return bulb + + +def _patch_discovery(prefix, no_device=False): + def _mocked_discovery(timeout=2, interface=False): + if no_device: + return [] + return [{"ip": IP_ADDRESS, "port": 55443, "capabilities": CAPABILITIES}] + + return patch(f"{prefix}.discover_bulbs", side_effect=_mocked_discovery) diff --git a/tests/components/yeelight/test_binary_sensor.py b/tests/components/yeelight/test_binary_sensor.py index bf20a7ec5b0..b3281168077 100644 --- a/tests/components/yeelight/test_binary_sensor.py +++ b/tests/components/yeelight/test_binary_sensor.py @@ -12,7 +12,9 @@ from tests.async_mock import patch async def test_nightlight(hass: HomeAssistant): """Test nightlight sensor.""" mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb + ): await async_setup_component(hass, DOMAIN, YAML_CONFIGURATION) await hass.async_block_till_done() diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py new file mode 100644 index 00000000000..7d1e51afbb1 --- /dev/null +++ b/tests/components/yeelight/test_config_flow.py @@ -0,0 +1,261 @@ +"""Test the Yeelight config flow.""" +from homeassistant import config_entries +from homeassistant.components.yeelight import ( + CONF_DEVICE, + CONF_MODE_MUSIC, + CONF_MODEL, + CONF_NIGHTLIGHT_SWITCH, + CONF_NIGHTLIGHT_SWITCH_TYPE, + CONF_SAVE_ON_CHANGE, + CONF_TRANSITION, + DEFAULT_MODE_MUSIC, + DEFAULT_NAME, + DEFAULT_NIGHTLIGHT_SWITCH, + DEFAULT_SAVE_ON_CHANGE, + DEFAULT_TRANSITION, + DOMAIN, + NIGHTLIGHT_SWITCH_TYPE_LIGHT, +) +from homeassistant.const import CONF_ID, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.core import HomeAssistant + +from . import ( + ID, + IP_ADDRESS, + MODULE, + MODULE_CONFIG_FLOW, + NAME, + _mocked_bulb, + _patch_discovery, +) + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry + +DEFAULT_CONFIG = { + CONF_NAME: NAME, + CONF_MODEL: "", + CONF_TRANSITION: DEFAULT_TRANSITION, + CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, + CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, + CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH, +} + + +async def test_discovery(hass: HomeAssistant): + """Test setting up discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + with patch(f"{MODULE}.async_setup", return_value=True) as mock_setup, patch( + f"{MODULE}.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_DEVICE: ID} + ) + + assert result3["type"] == "create_entry" + assert result3["title"] == NAME + assert result3["data"] == {CONF_ID: ID} + await hass.async_block_till_done() + mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() + + # ignore configured devices + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_discovery_no_device(hass: HomeAssistant): + """Test discovery without device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with _patch_discovery(f"{MODULE_CONFIG_FLOW}.yeelight", no_device=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_import(hass: HomeAssistant): + """Test import from yaml.""" + config = { + CONF_NAME: DEFAULT_NAME, + CONF_IP_ADDRESS: IP_ADDRESS, + CONF_TRANSITION: DEFAULT_TRANSITION, + CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, + CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, + CONF_NIGHTLIGHT_SWITCH_TYPE: NIGHTLIGHT_SWITCH_TYPE_LIGHT, + } + + # Cannot connect + mocked_bulb = _mocked_bulb(cannot_connect=True) + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + type(mocked_bulb).get_capabilities.assert_called_once() + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + # Success + mocked_bulb = _mocked_bulb() + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_setup, patch( + f"{MODULE}.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=config + ) + type(mocked_bulb).get_capabilities.assert_called_once() + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_NAME + assert result["data"] == { + CONF_NAME: DEFAULT_NAME, + CONF_IP_ADDRESS: IP_ADDRESS, + CONF_TRANSITION: DEFAULT_TRANSITION, + CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, + CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, + CONF_NIGHTLIGHT_SWITCH: True, + } + await hass.async_block_till_done() + mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() + + # Duplicate + mocked_bulb = _mocked_bulb() + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_manual(hass: HomeAssistant): + """Test manually setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + # Cannot connect (timeout) + mocked_bulb = _mocked_bulb(cannot_connect=True) + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: IP_ADDRESS} + ) + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + # Cannot connect (error) + type(mocked_bulb).get_capabilities = MagicMock(side_effect=OSError) + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: IP_ADDRESS} + ) + assert result3["errors"] == {"base": "cannot_connect"} + + # Success + mocked_bulb = _mocked_bulb() + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch( + f"{MODULE}.async_setup", return_value=True + ), patch( + f"{MODULE}.async_setup_entry", + return_value=True, + ): + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: IP_ADDRESS} + ) + assert result4["type"] == "create_entry" + assert result4["data"] == {CONF_IP_ADDRESS: IP_ADDRESS} + + # Duplicate + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mocked_bulb = _mocked_bulb() + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: IP_ADDRESS} + ) + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_options(hass: HomeAssistant): + """Test options flow.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: IP_ADDRESS}) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + config = { + CONF_MODEL: "", + CONF_TRANSITION: DEFAULT_TRANSITION, + CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, + CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, + CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH, + } + assert config_entry.options == { + CONF_NAME: "", + **config, + } + assert hass.states.get(f"light.{NAME}_nightlight") is None + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + + config[CONF_NIGHTLIGHT_SWITCH] = True + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], config + ) + await hass.async_block_till_done() + assert result2["type"] == "create_entry" + assert result2["data"] == { + CONF_NAME: "", + **config, + } + assert result2["data"] == config_entry.options + assert hass.states.get(f"light.{NAME}_nightlight") is not None diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py new file mode 100644 index 00000000000..004efed8deb --- /dev/null +++ b/tests/components/yeelight/test_init.py @@ -0,0 +1,69 @@ +"""Test Yeelight.""" +from homeassistant.components.yeelight import ( + CONF_NIGHTLIGHT_SWITCH_TYPE, + DOMAIN, + NIGHTLIGHT_SWITCH_TYPE_LIGHT, +) +from homeassistant.const import CONF_DEVICES, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import ( + CONFIG_ENTRY_DATA, + IP_ADDRESS, + MODULE, + MODULE_CONFIG_FLOW, + NAME, + _mocked_bulb, + _patch_discovery, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_setup_discovery(hass: HomeAssistant): + """Test setting up Yeelight by discovery.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"binary_sensor.{NAME}_nightlight") is not None + assert hass.states.get(f"light.{NAME}") is not None + + # Unload + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert hass.states.get(f"binary_sensor.{NAME}_nightlight") is None + assert hass.states.get(f"light.{NAME}") is None + + +async def test_setup_import(hass: HomeAssistant): + """Test import from yaml.""" + mocked_bulb = _mocked_bulb() + name = "yeelight" + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb + ): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DEVICES: { + IP_ADDRESS: { + CONF_NAME: name, + CONF_NIGHTLIGHT_SWITCH_TYPE: NIGHTLIGHT_SWITCH_TYPE_LIGHT, + } + } + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get(f"binary_sensor.{name}_nightlight") is not None + assert hass.states.get(f"light.{name}") is not None + assert hass.states.get(f"light.{name}_nightlight") is not None diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index c44c343e51b..aafe45851d3 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -34,10 +34,15 @@ from homeassistant.components.yeelight import ( ATTR_TRANSITIONS, CONF_CUSTOM_EFFECTS, CONF_FLOW_PARAMS, - CONF_NIGHTLIGHT_SWITCH_TYPE, + CONF_MODE_MUSIC, + CONF_NIGHTLIGHT_SWITCH, + CONF_SAVE_ON_CHANGE, + CONF_TRANSITION, + DEFAULT_MODE_MUSIC, + DEFAULT_NIGHTLIGHT_SWITCH, + DEFAULT_SAVE_ON_CHANGE, DEFAULT_TRANSITION, DOMAIN, - NIGHTLIGHT_SWITCH_TYPE_LIGHT, YEELIGHT_HSV_TRANSACTION, YEELIGHT_RGB_TRANSITION, YEELIGHT_SLEEP_TRANSACTION, @@ -66,7 +71,7 @@ from homeassistant.components.yeelight.light import ( YEELIGHT_MONO_EFFECT_LIST, YEELIGHT_TEMP_ONLY_EFFECT_LIST, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_DEVICES, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_ID, CONF_IP_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.color import ( @@ -79,24 +84,38 @@ from homeassistant.util.color import ( ) from . import ( - CAPABILITIES, ENTITY_LIGHT, ENTITY_NIGHTLIGHT, + IP_ADDRESS, MODULE, NAME, PROPERTIES, - YAML_CONFIGURATION, _mocked_bulb, + _patch_discovery, ) from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry async def test_services(hass: HomeAssistant, caplog): """Test Yeelight services.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ID: "", + CONF_IP_ADDRESS: IP_ADDRESS, + CONF_TRANSITION: DEFAULT_TRANSITION, + CONF_MODE_MUSIC: True, + CONF_SAVE_ON_CHANGE: True, + CONF_NIGHTLIGHT_SWITCH: True, + }, + ) + config_entry.add_to_hass(hass) + mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): - await async_setup_component(hass, DOMAIN, YAML_CONFIGURATION) + with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() async def _async_test_service(service, data, method, payload=None, domain=DOMAIN): @@ -264,70 +283,70 @@ async def test_services(hass: HomeAssistant, caplog): async def test_device_types(hass: HomeAssistant): """Test different device types.""" + mocked_bulb = _mocked_bulb() properties = {**PROPERTIES} properties.pop("active_mode") properties["color_mode"] = "3" + mocked_bulb.last_properties = properties - def _create_mocked_bulb(bulb_type, model, unique_id): - capabilities = {**CAPABILITIES} - capabilities["id"] = f"yeelight.{unique_id}" - mocked_bulb = _mocked_bulb() - mocked_bulb.bulb_type = bulb_type - mocked_bulb.last_properties = properties - mocked_bulb.capabilities = capabilities - model_specs = _MODEL_SPECS.get(model) - type(mocked_bulb).get_model_specs = MagicMock(return_value=model_specs) - return mocked_bulb - - types = { - "default": (None, "mono"), - "white": (BulbType.White, "mono"), - "color": (BulbType.Color, "color"), - "white_temp": (BulbType.WhiteTemp, "ceiling1"), - "white_temp_mood": (BulbType.WhiteTempMood, "ceiling4"), - "ambient": (BulbType.WhiteTempMood, "ceiling4"), - } - - devices = {} - mocked_bulbs = [] - unique_id = 0 - for name, (bulb_type, model) in types.items(): - devices[f"{name}.yeelight"] = {CONF_NAME: name} - devices[f"{name}_nightlight.yeelight"] = { - CONF_NAME: f"{name}_nightlight", - CONF_NIGHTLIGHT_SWITCH_TYPE: NIGHTLIGHT_SWITCH_TYPE_LIGHT, - } - mocked_bulbs.append(_create_mocked_bulb(bulb_type, model, unique_id)) - mocked_bulbs.append(_create_mocked_bulb(bulb_type, model, unique_id + 1)) - unique_id += 2 - - with patch(f"{MODULE}.Bulb", side_effect=mocked_bulbs): - await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DEVICES: devices}}) - await hass.async_block_till_done() + async def _async_setup(config_entry): + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() async def _async_test( - name, bulb_type, model, target_properties, nightlight_properties=None, - entity_name=None, - entity_id=None, + name=NAME, + entity_id=ENTITY_LIGHT, ): - if entity_id is None: - entity_id = f"light.{name}" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ID: "", + CONF_IP_ADDRESS: IP_ADDRESS, + CONF_TRANSITION: DEFAULT_TRANSITION, + CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, + CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, + CONF_NIGHTLIGHT_SWITCH: False, + }, + ) + config_entry.add_to_hass(hass) + + mocked_bulb.bulb_type = bulb_type + model_specs = _MODEL_SPECS.get(model) + type(mocked_bulb).get_model_specs = MagicMock(return_value=model_specs) + await _async_setup(config_entry) + state = hass.states.get(entity_id) assert state.state == "on" - target_properties["friendly_name"] = entity_name or name + target_properties["friendly_name"] = name target_properties["flowing"] = False target_properties["night_light"] = True assert dict(state.attributes) == target_properties + await hass.config_entries.async_unload(config_entry.entry_id) + await config_entry.async_remove(hass) + # nightlight if nightlight_properties is None: return - name += "_nightlight" - entity_id = f"light.{name}" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ID: "", + CONF_IP_ADDRESS: IP_ADDRESS, + CONF_TRANSITION: DEFAULT_TRANSITION, + CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, + CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, + CONF_NIGHTLIGHT_SWITCH: True, + }, + ) + config_entry.add_to_hass(hass) + await _async_setup(config_entry) + assert hass.states.get(entity_id).state == "off" state = hass.states.get(f"{entity_id}_nightlight") assert state.state == "on" @@ -337,6 +356,9 @@ async def test_device_types(hass: HomeAssistant): nightlight_properties["night_light"] = True assert dict(state.attributes) == nightlight_properties + await hass.config_entries.async_unload(config_entry.entry_id) + await config_entry.async_remove(hass) + bright = round(255 * int(PROPERTIES["bright"]) / 100) current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 100) ct = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"])) @@ -355,7 +377,6 @@ async def test_device_types(hass: HomeAssistant): # Default await _async_test( - "default", None, "mono", { @@ -367,7 +388,6 @@ async def test_device_types(hass: HomeAssistant): # White await _async_test( - "white", BulbType.White, "mono", { @@ -380,7 +400,6 @@ async def test_device_types(hass: HomeAssistant): # Color model_specs = _MODEL_SPECS["color"] await _async_test( - "color", BulbType.Color, "color", { @@ -404,7 +423,6 @@ async def test_device_types(hass: HomeAssistant): # WhiteTemp model_specs = _MODEL_SPECS["ceiling1"] await _async_test( - "white_temp", BulbType.WhiteTemp, "ceiling1", { @@ -427,9 +445,10 @@ async def test_device_types(hass: HomeAssistant): ) # WhiteTempMood + properties.pop("power") + properties["main_power"] = "on" model_specs = _MODEL_SPECS["ceiling4"] await _async_test( - "white_temp_mood", BulbType.WhiteTempMood, "ceiling4", { @@ -454,7 +473,6 @@ async def test_device_types(hass: HomeAssistant): }, ) await _async_test( - "ambient", BulbType.WhiteTempMood, "ceiling4", { @@ -468,36 +486,52 @@ async def test_device_types(hass: HomeAssistant): "rgb_color": bg_rgb_color, "xy_color": bg_xy_color, }, - entity_name="ambient ambilight", - entity_id="light.ambient_ambilight", + name=f"{NAME} ambilight", + entity_id=f"{ENTITY_LIGHT}_ambilight", ) async def test_effects(hass: HomeAssistant): """Test effects.""" - yaml_configuration = { - DOMAIN: { - CONF_DEVICES: YAML_CONFIGURATION[DOMAIN][CONF_DEVICES], - CONF_CUSTOM_EFFECTS: [ - { - CONF_NAME: "mock_effect", - CONF_FLOW_PARAMS: { - ATTR_COUNT: 3, - ATTR_TRANSITIONS: [ - {YEELIGHT_HSV_TRANSACTION: [300, 50, 500, 50]}, - {YEELIGHT_RGB_TRANSITION: [100, 100, 100, 300, 30]}, - {YEELIGHT_TEMPERATURE_TRANSACTION: [3000, 200, 20]}, - {YEELIGHT_SLEEP_TRANSACTION: [800]}, - ], + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_CUSTOM_EFFECTS: [ + { + CONF_NAME: "mock_effect", + CONF_FLOW_PARAMS: { + ATTR_COUNT: 3, + ATTR_TRANSITIONS: [ + {YEELIGHT_HSV_TRANSACTION: [300, 50, 500, 50]}, + {YEELIGHT_RGB_TRANSITION: [100, 100, 100, 300, 30]}, + {YEELIGHT_TEMPERATURE_TRANSACTION: [3000, 200, 20]}, + {YEELIGHT_SLEEP_TRANSACTION: [800]}, + ], + }, }, - }, - ], - } - } + ], + }, + }, + ) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ID: "", + CONF_IP_ADDRESS: IP_ADDRESS, + CONF_TRANSITION: DEFAULT_TRANSITION, + CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, + CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, + CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH, + }, + ) + config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() - with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): - assert await async_setup_component(hass, DOMAIN, yaml_configuration) + with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get(ENTITY_LIGHT).attributes.get( From 7e87181826b98159d16244829953d28b024f6e68 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 31 Aug 2020 16:55:59 +0200 Subject: [PATCH 529/862] Bump gios library to version 0.1.3 (#39507) --- homeassistant/components/gios/__init__.py | 2 -- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index ae3030ac88c..005cc4c9c26 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -70,6 +70,4 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator): InvalidSensorsData, ) as error: raise UpdateFailed(error) from error - if not self.gios.data: - raise UpdateFailed("Invalid sensors data") return self.gios.data diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 527bb7e116f..da4ceddbeb5 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -3,6 +3,6 @@ "name": "GIOŚ", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], - "requirements": ["gios==0.1.1"], + "requirements": ["gios==0.1.3"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 56c53235f58..cbcbff65a9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -653,7 +653,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.1.1 +gios==0.1.3 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98cd6b9105d..c1168ee7dac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.1.1 +gios==0.1.3 # homeassistant.components.glances glances_api==0.2.0 From 5de1d04b6e515f7044b47eb39d3f228bd92389eb Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 31 Aug 2020 18:18:12 +0200 Subject: [PATCH 530/862] Bump dependency to 0.11.0 for HomematicIP Cloud (#39508) * Bump dependency to 0.11.0 for HomematicIP Cloud * Update test data --- .../homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homematicip_cloud/test_device.py | 2 +- tests/fixtures/homematicip_cloud.json | 924 +++++++++++++++++- 5 files changed, 927 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b4999f82006..3ecf668e449 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==0.10.19"], + "requirements": ["homematicip==0.11.0"], "codeowners": ["@SukramJ"], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index cbcbff65a9a..34be3c89af7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -752,7 +752,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.5 # homeassistant.components.homematicip_cloud -homematicip==0.10.19 +homematicip==0.11.0 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1168ee7dac..fd6ebd4f0e2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -379,7 +379,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.5 # homeassistant.components.homematicip_cloud -homematicip==0.10.19 +homematicip==0.11.0 # homeassistant.components.google # homeassistant.components.remember_the_milk diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index f43e0ddafae..0154eb7b327 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 187 + assert len(mock_hap.hmip_device_by_entity_id) == 190 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 1aa3bfa48ad..4060ca7a820 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -14,6 +14,848 @@ } }, "devices": { + "3014F7110TILTVIBRATIONSENSOR": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.16", + "firmwareVersionInteger": 65552, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110TILTVIBRATIONSENSOR", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -59, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "accelerationSensorEventFilterPeriod": 0.5, + "accelerationSensorMode": "FLAT_DECT", + "accelerationSensorNeutralPosition": "VERTICAL", + "accelerationSensorSensitivity": "SENSOR_RANGE_2G", + "accelerationSensorTriggerAngle": 45, + "accelerationSensorTriggered": true, + "deviceId": "3014F7110TILTVIBRATIONSENSOR", + "functionalChannelType": "TILT_VIBRATION_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110TILTVIBRATIONSENSOR", + "label": "Garage Neigungs- und Ersch\u00fctterungssensor", + "lastStatusUpdate": 1598610615630, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 378, + "modelType": "HmIP-STV", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110TILTVIBRATIONSENSOR", + "type": "TILT_VIBRATION_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F711000WIREDSWITCH8": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_WIRED", + "firmwareVersion": "1.2.4", + "firmwareVersionInteger": 66052, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F711000WIREDSWITCH8", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": null, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": true, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": true + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F711000WIREDSWITCH8", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "Fernseher (Wohnzimmer)", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711000WIREDSWITCH8", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "Steckdosen (Wohnzimmer)", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711000WIREDSWITCH8", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "Steckdosen Kaffeeecke (K\u00fcche)", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "4": { + "deviceId": "3014F711000WIREDSWITCH8", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 4, + "label": "Steckdosen (K\u00fcche)", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "5": { + "deviceId": "3014F711000WIREDSWITCH8", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 5, + "groups": [], + "index": 5, + "label": "T\u00fcrlicht (Garten)", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "6": { + "deviceId": "3014F711000WIREDSWITCH8", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 6, + "groups": [], + "index": 6, + "label": "Arbeitsplattenlicht (K\u00fcche)", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "7": { + "deviceId": "3014F711000WIREDSWITCH8", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 7, + "groups": [], + "index": 7, + "label": "Raumlich (Flur/Oben)", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "8": { + "deviceId": "3014F711000WIREDSWITCH8", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 8, + "groups": [], + "index": 8, + "label": "Spiegellicht (Bad/Oben)", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000WIREDSWITCH8", + "label": "Wired Schaltaktor \u2013 8-fach", + "lastStatusUpdate": 1595225535806, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 343, + "modelType": "HmIPW-DRS8", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711000WIREDSWITCH8", + "type": "WIRED_SWITCH_8", + "updateState": "UP_TO_DATE" + }, + "3014F711000WIREDDIMMER3": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_WIRED", + "firmwareVersion": "1.0.0", + "firmwareVersionInteger": 65536, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F711000WIREDDIMMER3", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": null, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": true, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": true + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "coProFaulty": false, + "coProRestartNeeded": false, + "deviceId": "3014F711000WIREDDIMMER3", + "deviceOverheated": false, + "deviceOverloaded": false, + "dimLevel": 0.0, + "functionalChannelType": "DIMMER_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "Raumlich (K\u00fcche)", + "on": false, + "profileMode": "AUTOMATIC", + "supportedOptionalFeatures": { + "IFeatureDeviceOverloaded": true + }, + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "coProFaulty": false, + "coProRestartNeeded": false, + "deviceId": "3014F711000WIREDDIMMER3", + "deviceOverheated": false, + "deviceOverloaded": false, + "dimLevel": 0.0, + "functionalChannelType": "DIMMER_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "Raumlicht (Bad/Oben)", + "on": false, + "profileMode": "AUTOMATIC", + "supportedOptionalFeatures": { + "IFeatureDeviceOverloaded": true + }, + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "coProFaulty": false, + "coProRestartNeeded": false, + "deviceId": "3014F711000WIREDDIMMER3", + "deviceOverheated": false, + "deviceOverloaded": false, + "dimLevel": 0.0, + "functionalChannelType": "DIMMER_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "Raumlich (Kinderzimmer/Oben)", + "on": false, + "profileMode": "AUTOMATIC", + "supportedOptionalFeatures": { + "IFeatureDeviceOverloaded": true + }, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000WIREDDIMMER3", + "label": "Wired Dimmaktor \u2013 3-fach (K\u00fcche)", + "lastStatusUpdate": 1595225686220, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 345, + "modelType": "HmIPW-DRD3", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711000WIREDDIMMER3", + "type": "WIRED_DIMMER_3", + "updateState": "UP_TO_DATE" + }, + "3014F711000WIREDINPUT32": { + "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_WIRED", + "firmwareVersion": "1.2.2", + "firmwareVersionInteger": 66050, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F711000WIREDINPUT32", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": null, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": true, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": true + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "10": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 10, + "groups": [], + "index": 10, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "11": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 11, + "groups": [], + "index": 11, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "12": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 12, + "groups": [], + "index": 12, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "13": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 13, + "groups": [], + "index": 13, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "14": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 14, + "groups": [], + "index": 14, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "15": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 15, + "groups": [], + "index": 15, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "16": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 16, + "groups": [], + "index": 16, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "17": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 17, + "groups": [], + "index": 17, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "18": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 18, + "groups": [], + "index": 18, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "19": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 19, + "groups": [], + "index": 19, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "2": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "20": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 20, + "groups": [], + "index": 20, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "21": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 21, + "groups": [], + "index": 21, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "22": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 22, + "groups": [], + "index": 22, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "23": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 23, + "groups": [], + "index": 23, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "24": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 24, + "groups": [], + "index": 24, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "25": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 25, + "groups": [], + "index": 25, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "26": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 26, + "groups": [], + "index": 26, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "27": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 27, + "groups": [], + "index": 27, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "28": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 28, + "groups": [], + "index": 28, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "29": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 29, + "groups": [], + "index": 29, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "3": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "30": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 30, + "groups": [], + "index": 30, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "31": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 31, + "groups": [], + "index": 31, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "32": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 32, + "groups": [], + "index": 32, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "4": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 4, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "5": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 5, + "groups": [], + "index": 5, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "6": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 6, + "groups": [], + "index": 6, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "7": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 7, + "groups": [], + "index": 7, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "8": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 8, + "groups": [], + "index": 8, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + }, + "9": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F711000WIREDINPUT32", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 9, + "groups": [], + "index": 9, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000WIREDINPUT32", + "label": "Wired Eingangsmodul \u2013 32-fach", + "lastStatusUpdate": 1595222885844, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 347, + "modelType": "HmIPW-DRI32", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711000WIREDINPUT32", + "type": "WIRED_INPUT_32", + "updateState": "UP_TO_DATE" + }, "3014F7110SHUTTER_OPTICAL": { "availableFirmwareVersion": "1.16.10", "connectionType": "HMIP_RF", @@ -88,6 +930,7 @@ "3014F7110000000HmIPFSI16": { "availableFirmwareVersion": "0.0.0", "connectionType": "HMIP_RF", + "connectionType": "HMIP_RF", "firmwareVersion": "1.16.2", "firmwareVersionInteger": 69634, "functionalChannels": { @@ -156,6 +999,7 @@ }, "3014F7110000000HOERMANN": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.14", "firmwareVersionInteger": 65550, "functionalChannels": { @@ -220,6 +1064,7 @@ }, "3014F711000BBBB000000000": { "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", "firmwareVersion": "2.0.2", "firmwareVersionInteger": 131074, "functionalChannels": { @@ -287,6 +1132,7 @@ }, "3014F711000000BBBB000005": { "availableFirmwareVersion": "1.0.16", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.12", "firmwareVersionInteger": 65548, "functionalChannels": { @@ -350,6 +1196,7 @@ }, "3014F711000000000000AAA5": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.12", "firmwareVersionInteger": 65548, "functionalChannels": { @@ -417,6 +1264,7 @@ }, "3014F7110000000000ABCD50": { "availableFirmwareVersion": "1.0.12", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.12", "firmwareVersionInteger": 65548, "functionalChannels": { @@ -481,6 +1329,7 @@ }, "3014F7110000000000000049": { "availableFirmwareVersion": "1.0.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.8", "firmwareVersionInteger": 65544, "functionalChannels": { @@ -674,6 +1523,7 @@ }, "3014F7110000000000000148": { "availableFirmwareVersion": "1.4.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.4.0", "firmwareVersionInteger": 66560, "functionalChannels": { @@ -756,6 +1606,7 @@ }, "3014F7110000ABCDABCD0033": { "availableFirmwareVersion": "1.0.6", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.6", "firmwareVersionInteger": 65542, "functionalChannels": { @@ -818,6 +1669,7 @@ }, "3014F7110000000000000031": { "availableFirmwareVersion": "1.2.1", + "connectionType": "HMIP_RF", "firmwareVersion": "1.2.1", "firmwareVersionInteger": 66049, "functionalChannels": { @@ -886,6 +1738,7 @@ }, "3014F7110000000000000052": { "availableFirmwareVersion": "1.0.5", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.5", "firmwareVersionInteger": 65541, "functionalChannels": { @@ -1004,6 +1857,7 @@ }, "3014F71100000000FAL24C10": { "availableFirmwareVersion": "1.6.2", + "connectionType": "HMIP_RF", "firmwareVersion": "1.6.2", "firmwareVersionInteger": 67074, "functionalChannels": { @@ -1161,6 +2015,7 @@ }, "3014F71100000000000BBL24": { "availableFirmwareVersion": "1.6.2", + "connectionType": "HMIP_RF", "firmwareVersion": "1.6.2", "firmwareVersionInteger": 67074, "functionalChannels": { @@ -1227,6 +2082,7 @@ }, "3014F7110000000000BCBB11": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.10.10", "firmwareVersionInteger": 68106, "functionalChannels": { @@ -1290,6 +2146,7 @@ }, "3014F711ABCD0ABCD000002": { "availableFirmwareVersion": "1.6.4", + "connectionType": "HMIP_RF", "firmwareVersion": "1.6.4", "firmwareVersionInteger": 67076, "functionalChannels": { @@ -1378,6 +2235,7 @@ "3014F71100000000ABCDEF10": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.6", "firmwareVersionInteger": 65542, "functionalChannels": { @@ -1433,6 +2291,7 @@ }, "3014F71100000000000TEST1": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.8.8", "firmwareVersionInteger": 67592, "functionalChannels": { @@ -1489,6 +2348,7 @@ }, "3014F7110000000000000064": { "availableFirmwareVersion": "1.0.6", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.6", "firmwareVersionInteger": 65542, "functionalChannels": { @@ -1561,6 +2421,7 @@ }, "3014F711BADCAFE000000001": { "availableFirmwareVersion": "1.2.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.2.0", "firmwareVersionInteger": 66048, "functionalChannels": { @@ -1626,6 +2487,7 @@ }, "3014F7110000000000000055": { "availableFirmwareVersion": "1.2.4", + "connectionType": "HMIP_RF", "firmwareVersion": "1.2.4", "firmwareVersionInteger": 66052, "functionalChannels": { @@ -1698,6 +2560,7 @@ }, "3014F711ABCDEF0000000014": { "availableFirmwareVersion": "1.4.2", + "connectionType": "HMIP_RF", "firmwareVersion": "1.4.2", "firmwareVersionInteger": 66562, "functionalChannels": { @@ -1769,6 +2632,7 @@ }, "3014F711BSL0000000000050": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.2", "firmwareVersionInteger": 65538, "functionalChannels": { @@ -1843,6 +2707,7 @@ }, "3014F711SLO0000000000026": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.16", "firmwareVersionInteger": 65552, "functionalChannels": { @@ -1892,6 +2757,7 @@ }, "3014F7110000000000000054": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.0", "firmwareVersionInteger": 65536, "functionalChannels": { @@ -1949,6 +2815,7 @@ }, "3014F711000000000AAAAA25": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.12", "firmwareVersionInteger": 65548, "functionalChannels": { @@ -2025,6 +2892,7 @@ }, "3014F7110000000000000038": { "availableFirmwareVersion": "1.0.18", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.18", "firmwareVersionInteger": 65554, "functionalChannels": { @@ -2088,6 +2956,7 @@ }, "3014F7110000000000BBBBB1": { "availableFirmwareVersion": "1.6.2", + "connectionType": "HMIP_RF", "firmwareVersion": "1.6.2", "firmwareVersionInteger": 67074, "functionalChannels": { @@ -2216,6 +3085,7 @@ }, "3014F7110000000000BBBBB8": { "availableFirmwareVersion": "1.2.16", + "connectionType": "HMIP_RF", "firmwareVersion": "1.2.16", "firmwareVersionInteger": 66064, "functionalChannels": { @@ -2264,6 +3134,7 @@ }, "3014F711000000000000BB11": { "availableFirmwareVersion": "1.4.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.4.8", "firmwareVersionInteger": 66568, "functionalChannels": { @@ -2316,6 +3187,7 @@ }, "3014F71100000000000BBB17": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.2", "firmwareVersionInteger": 65538, "functionalChannels": { @@ -2369,6 +3241,7 @@ }, "3014F7110000000000000050": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.2", "firmwareVersionInteger": "65538", "functionalChannels": { @@ -2427,6 +3300,7 @@ }, "3014F7110000000000000000": { "availableFirmwareVersion": "1.16.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.16.8", "firmwareVersionInteger": 69640, "functionalChannels": { @@ -2482,6 +3356,7 @@ }, "3014F7110000000000005551": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.2.12", "firmwareVersionInteger": 66060, "functionalChannels": { @@ -2532,6 +3407,7 @@ }, "3014F7110000000000000001": { "availableFirmwareVersion": "1.16.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.16.8", "firmwareVersionInteger": 69640, "functionalChannels": { @@ -2587,6 +3463,7 @@ }, "3014F7110000000000000002": { "availableFirmwareVersion": "1.16.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.16.8", "firmwareVersionInteger": 69640, "functionalChannels": { @@ -2642,6 +3519,7 @@ }, "3014F7110000000000000003": { "availableFirmwareVersion": "1.16.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.16.8", "firmwareVersionInteger": 69640, "functionalChannels": { @@ -2697,6 +3575,7 @@ }, "3014F7110000000000000004": { "availableFirmwareVersion": "1.16.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.16.8", "firmwareVersionInteger": 69640, "functionalChannels": { @@ -2752,6 +3631,7 @@ }, "3014F7110000000000000005": { "availableFirmwareVersion": "1.16.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.16.8", "firmwareVersionInteger": 69640, "functionalChannels": { @@ -2807,6 +3687,7 @@ }, "3014F7110000000000000006": { "availableFirmwareVersion": "1.16.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.16.8", "firmwareVersionInteger": 69640, "functionalChannels": { @@ -2861,6 +3742,7 @@ }, "3014F7110000000000000007": { "availableFirmwareVersion": "1.16.8", + "connectionType": "HMIP_RF", "firmwareVersion": "1.16.8", "firmwareVersionInteger": 69640, "functionalChannels": { @@ -2915,6 +3797,7 @@ }, "3014F7110000000000000108": { "availableFirmwareVersion": "1.12.6", + "connectionType": "HMIP_RF", "firmwareVersion": "1.12.6", "firmwareVersionInteger": 68614, "functionalChannels": { @@ -2984,6 +3867,7 @@ }, "3014F7110000000000000109": { "availableFirmwareVersion": "1.6.2", + "connectionType": "HMIP_RF", "firmwareVersion": "1.6.2", "firmwareVersionInteger": 67074, "functionalChannels": { @@ -3053,6 +3937,7 @@ }, "3014F7110000000000000008": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "2.6.2", "firmwareVersionInteger": 132610, "functionalChannels": { @@ -3107,6 +3992,7 @@ }, "3014F7110000000000000009": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "2.6.2", "firmwareVersionInteger": 132610, "functionalChannels": { @@ -3161,6 +4047,7 @@ }, "3014F7110000000000000010": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "2.6.2", "firmwareVersionInteger": 132610, "functionalChannels": { @@ -3215,6 +4102,7 @@ }, "3014F7110000000000000110": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "2.6.2", "firmwareVersionInteger": 132610, "functionalChannels": { @@ -3268,6 +4156,7 @@ "3014F7110000000000000011": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", "firmwareVersion": "2.0.2", "firmwareVersionInteger": 131074, "functionalChannels": { @@ -3324,6 +4213,7 @@ "3014F7110000000000000012": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", "firmwareVersion": "2.0.2", "firmwareVersionInteger": 131074, "functionalChannels": { @@ -3380,6 +4270,7 @@ "3014F7110000000000000013": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", "firmwareVersion": "2.0.2", "firmwareVersionInteger": 131074, "functionalChannels": { @@ -3436,6 +4327,7 @@ "3014F7110000000000000014": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", "firmwareVersion": "2.0.2", "firmwareVersionInteger": 131074, "functionalChannels": { @@ -3492,6 +4384,7 @@ "3014F7110000000000000015": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", "firmwareVersion": "2.0.2", "firmwareVersionInteger": 131074, "functionalChannels": { @@ -3548,6 +4441,7 @@ "3014F7110000000000000016": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", "firmwareVersion": "2.0.2", "firmwareVersionInteger": 131074, "functionalChannels": { @@ -3604,6 +4498,7 @@ "3014F7110000000000000017": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", + "connectionType": "HMIP_RF", "firmwareVersion": "2.0.2", "firmwareVersionInteger": 131074, "functionalChannels": { @@ -3659,6 +4554,7 @@ }, "3014F7110000000000000018": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.11", "firmwareVersionInteger": 65547, "functionalChannels": { @@ -3711,6 +4607,7 @@ }, "3014F7110000000000000019": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.11", "firmwareVersionInteger": 65547, "functionalChannels": { @@ -3763,6 +4660,7 @@ }, "3014F7110000000000000020": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.11", "firmwareVersionInteger": 65547, "functionalChannels": { @@ -3815,6 +4713,7 @@ }, "3014F7110000000000000021": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.11", "firmwareVersionInteger": 65547, "functionalChannels": { @@ -3867,6 +4766,7 @@ }, "3014F7110000000000000022": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.8.0", "firmwareVersionInteger": 67584, "functionalChannels": { @@ -3924,6 +4824,7 @@ }, "3014F7110000000000000023": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.8.0", "firmwareVersionInteger": 67584, "functionalChannels": { @@ -3981,6 +4882,7 @@ }, "3014F7110000000000000024": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.8.0", "firmwareVersionInteger": 67584, "functionalChannels": { @@ -4038,6 +4940,7 @@ }, "3014F7110000000000000025": { "availableFirmwareVersion": "1.8.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.8.0", "firmwareVersionInteger": 67584, "functionalChannels": { @@ -4095,6 +4998,7 @@ }, "3014F7110000000000000029": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.14", "firmwareVersionInteger": 65550, "functionalChannels": { @@ -4147,6 +5051,7 @@ }, "3014F711AAAA000000000001": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.10", "firmwareVersionInteger": 65546, "functionalChannels": { @@ -4215,6 +5120,7 @@ }, "3014F711AAAA000000000002": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.6", "firmwareVersionInteger": 65542, "functionalChannels": { @@ -4267,6 +5173,7 @@ }, "3014F711AAAA000000000003": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.10", "firmwareVersionInteger": 65546, "functionalChannels": { @@ -4324,6 +5231,7 @@ }, "3014F711AAAA000000000004": { "availableFirmwareVersion": "1.2.10", + "connectionType": "HMIP_RF", "firmwareVersion": "1.2.10", "firmwareVersionInteger": 66058, "functionalChannels": { @@ -4372,6 +5280,7 @@ }, "3014F711AAAA000000000005": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.4.8", "firmwareVersionInteger": 66568, "functionalChannels": { @@ -4424,6 +5333,7 @@ }, "3014F711BBBBBBBBBBBBB017": { "availableFirmwareVersion": "1.0.19", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.19", "firmwareVersionInteger": 65555, "functionalChannels": { @@ -4516,6 +5426,7 @@ }, "3014F711BBBBBBBBBBBBB016": { "availableFirmwareVersion": "1.0.19", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.19", "firmwareVersionInteger": 65555, "functionalChannels": { @@ -4628,6 +5539,7 @@ }, "3014F711AAAAAAAAAAAAAA51": { "availableFirmwareVersion": "1.4.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.4.0", "firmwareVersionInteger": 66560, "functionalChannels": { @@ -4686,6 +5598,7 @@ }, "3014F711ACBCDABCADCA66": { "availableFirmwareVersion": "1.6.2", + "connectionType": "HMIP_RF", "firmwareVersion": "1.6.2", "firmwareVersionInteger": 67074, "functionalChannels": { @@ -4750,6 +5663,7 @@ }, "3014F711BBBBBBBBBBBBB18": { "availableFirmwareVersion": "0.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.8.12", "firmwareVersionInteger": 67596, "functionalChannels": { @@ -4894,6 +5808,7 @@ }, "3014F0000000000000FAF9B4": { "availableFirmwareVersion": "1.0.0", + "connectionType": "HMIP_RF", "firmwareVersion": "1.0.0", "firmwareVersionInteger": 65536, "functionalChannels": { @@ -6391,6 +7306,13 @@ } }, "home": { + "accessPointUpdateStates": { + "3014F711A000000BAD0C0DED": { + "accessPointUpdateState": "UP_TO_DATE", + "successfulUpdateTimestamp": 0, + "updateStateChangedTimestamp": 0 + } + }, "apExchangeClientId": null, "apExchangeState": "NONE", "availableAPVersion": null, @@ -6527,4 +7449,4 @@ "windSpeed": 8.568 } } -} +} \ No newline at end of file From 72fe4db937033691bea66be6ad47cc4cc634b877 Mon Sep 17 00:00:00 2001 From: On Freund Date: Mon, 31 Aug 2020 21:04:41 +0300 Subject: [PATCH 531/862] Update pyrisco to 0.2.4 (#39521) --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 2e360cd0c43..4a43365e3af 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", "requirements": [ - "pyrisco==0.2.3" + "pyrisco==0.2.4" ], "codeowners": [ "@OnFreund" diff --git a/requirements_all.txt b/requirements_all.txt index 34be3c89af7..969aa828af0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1588,7 +1588,7 @@ pyrecswitch==1.0.2 pyrepetier==3.0.5 # homeassistant.components.risco -pyrisco==0.2.3 +pyrisco==0.2.4 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd6ebd4f0e2..1b2f58823a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -766,7 +766,7 @@ pyps4-2ndscreen==1.1.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.2.3 +pyrisco==0.2.4 # homeassistant.components.acer_projector # homeassistant.components.zha From d8b4fa4a8b0587792a9f24e287b859265ba4cb5f Mon Sep 17 00:00:00 2001 From: On Freund Date: Mon, 31 Aug 2020 21:08:08 +0300 Subject: [PATCH 532/862] Update pyvolumio to 0.1.2 (#39522) --- homeassistant/components/volumio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/volumio/manifest.json b/homeassistant/components/volumio/manifest.json index c5d14859f05..08c0167df0a 100644 --- a/homeassistant/components/volumio/manifest.json +++ b/homeassistant/components/volumio/manifest.json @@ -5,5 +5,5 @@ "codeowners": ["@OnFreund"], "config_flow": true, "zeroconf": ["_Volumio._tcp.local."], - "requirements": ["pyvolumio==0.1.1"] + "requirements": ["pyvolumio==0.1.2"] } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 969aa828af0..fe54fcbf224 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1836,7 +1836,7 @@ pyvizio==0.1.51 pyvlx==0.2.16 # homeassistant.components.volumio -pyvolumio==0.1.1 +pyvolumio==0.1.2 # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b2f58823a0..8f968f16c73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -860,7 +860,7 @@ pyvesync==1.1.0 pyvizio==0.1.51 # homeassistant.components.volumio -pyvolumio==0.1.1 +pyvolumio==0.1.2 # homeassistant.components.html5 pywebpush==1.9.2 From 11e4ad22726265cfe4cb7835b45155a5f6df8cbb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2020 13:11:26 -0500 Subject: [PATCH 533/862] Bump zeroconf to 0.28.3 (#39471) Changes: https://github.com/jstasiak/python-zeroconf/pull/287 Hopefully this will fix the last round of HomeKit issues --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 79bef3bf956..787422ff1c2 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.2"], + "requirements": ["zeroconf==0.28.3"], "dependencies": ["api"], "codeowners": ["@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 60d40d73646..c8ff36872e6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ sqlalchemy==1.3.19 voluptuous-serialize==2.4.0 voluptuous==0.11.7 yarl==1.4.2 -zeroconf==0.28.2 +zeroconf==0.28.3 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index fe54fcbf224..8a2bf41eec5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2285,7 +2285,7 @@ youtube_dl==2020.07.28 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.2 +zeroconf==0.28.3 # homeassistant.components.zha zha-quirks==0.0.43 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f968f16c73..9b262b4ab88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1049,7 +1049,7 @@ xmltodict==0.12.0 yeelight==0.5.2 # homeassistant.components.zeroconf -zeroconf==0.28.2 +zeroconf==0.28.3 # homeassistant.components.zha zha-quirks==0.0.43 From 4828d3d85b9e2a7d60b8e2f99ced966ea17d145f Mon Sep 17 00:00:00 2001 From: On Freund Date: Mon, 31 Aug 2020 21:42:14 +0300 Subject: [PATCH 534/862] Update pyvolumio to 0.1.1 (#39525) --- homeassistant/components/kodi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index cc4aa0e88af..903c458464a 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,7 +2,7 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", - "requirements": ["pykodi==0.1"], + "requirements": ["pykodi==0.1.1"], "codeowners": [ "@armills", "@OnFreund" diff --git a/requirements_all.txt b/requirements_all.txt index 8a2bf41eec5..c7d090f9514 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1424,7 +1424,7 @@ pyitachip2ir==0.0.7 pykira==0.1.1 # homeassistant.components.kodi -pykodi==0.1 +pykodi==0.1.1 # homeassistant.components.kwb pykwb==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b262b4ab88..f864a2458ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -692,7 +692,7 @@ pyisy==2.0.2 pykira==0.1.1 # homeassistant.components.kodi -pykodi==0.1 +pykodi==0.1.1 # homeassistant.components.lastfm pylast==3.3.0 From 3fe8afe6674b2bb9c4c5c9889721a169e5cd09b6 Mon Sep 17 00:00:00 2001 From: Emily Mills Date: Mon, 31 Aug 2020 16:53:32 -0400 Subject: [PATCH 535/862] Removing myself as a kodi owner I no longer use Kodi --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6a2955f3c13..85cde0973f9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -224,7 +224,7 @@ homeassistant/components/keenetic_ndms2/* @foxel homeassistant/components/kef/* @basnijholt homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 @farmio @marvin-w -homeassistant/components/kodi/* @armills @OnFreund +homeassistant/components/kodi/* @OnFreund homeassistant/components/konnected/* @heythisisnate @kit-klein homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus From 60d16060491aed08fffebc7d10b9b0009867a9cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2020 16:18:12 -0500 Subject: [PATCH 536/862] Fix kodi codeowners (#39532) --- homeassistant/components/kodi/manifest.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 903c458464a..7141ca43346 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -4,9 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": ["pykodi==0.1.1"], "codeowners": [ - "@armills", "@OnFreund" ], "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."], "config_flow": true -} \ No newline at end of file +} From 8d68963854cc1737875be1771f6f4af034de4c14 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 1 Sep 2020 00:07:08 +0000 Subject: [PATCH 537/862] [ci skip] Translation update --- .../ambiclimate/translations/ru.json | 2 +- .../components/august/translations/ru.json | 2 +- .../components/avri/translations/ru.json | 2 +- .../components/awair/translations/ru.json | 2 +- .../azure_devops/translations/ru.json | 2 +- .../components/dexcom/translations/ru.json | 2 +- .../flick_electric/translations/ru.json | 2 +- .../components/flume/translations/ru.json | 2 +- .../garmin_connect/translations/ru.json | 2 +- .../components/icloud/translations/ru.json | 2 +- .../components/insteon/translations/ca.json | 25 +++++++++++ .../components/insteon/translations/es.json | 25 +++++++++++ .../components/insteon/translations/it.json | 42 +++++++++++++++---- .../components/insteon/translations/no.json | 41 ++++++++++++++---- .../components/insteon/translations/pt.json | 34 +++++++++++++++ .../components/insteon/translations/ru.json | 42 +++++++++++++++---- .../insteon/translations/zh-Hant.json | 42 +++++++++++++++---- .../components/juicenet/translations/ru.json | 2 +- .../components/kodi/translations/pt.json | 9 ++++ .../components/konnected/translations/ca.json | 4 +- .../components/life360/translations/ru.json | 4 +- .../components/mill/translations/ru.json | 2 +- .../components/nexia/translations/ru.json | 2 +- .../components/nzbget/translations/es.json | 36 ++++++++++++++++ .../components/nzbget/translations/it.json | 36 ++++++++++++++++ .../components/nzbget/translations/no.json | 36 ++++++++++++++++ .../components/nzbget/translations/pt.json | 36 ++++++++++++++++ .../ovo_energy/translations/ru.json | 2 +- .../plum_lightpad/translations/ru.json | 2 +- .../progettihwsw/translations/ca.json | 39 +++++++++++++++++ .../progettihwsw/translations/es.json | 39 +++++++++++++++++ .../progettihwsw/translations/it.json | 40 ++++++++++++++++++ .../progettihwsw/translations/no.json | 40 ++++++++++++++++++ .../progettihwsw/translations/pt.json | 40 ++++++++++++++++++ .../progettihwsw/translations/ru.json | 40 ++++++++++++++++++ .../progettihwsw/translations/zh-Hant.json | 40 ++++++++++++++++++ .../components/risco/translations/es.json | 9 ++++ .../components/risco/translations/it.json | 22 ++++++++++ .../components/risco/translations/pt.json | 32 ++++++++++++++ .../components/sharkiq/translations/ca.json | 20 +++++++++ .../components/sharkiq/translations/es.json | 20 +++++++++ .../components/sharkiq/translations/it.json | 20 +++++++++ .../components/sharkiq/translations/no.json | 20 +++++++++ .../components/sharkiq/translations/pt.json | 20 +++++++++ .../components/sharkiq/translations/ru.json | 2 +- .../sharkiq/translations/zh-Hant.json | 20 +++++++++ .../components/shelly/translations/pt.json | 24 +++++++++++ .../simplisafe/translations/ru.json | 2 +- .../components/spotify/translations/it.json | 7 +++- .../components/spotify/translations/pt.json | 13 ++++++ .../components/tibber/translations/ru.json | 2 +- .../totalconnect/translations/ru.json | 2 +- .../components/wilight/translations/pt.json | 16 +++++++ .../components/yeelight/translations/en.json | 8 ++-- .../components/yeelight/translations/it.json | 39 +++++++++++++++++ .../components/yeelight/translations/ru.json | 39 +++++++++++++++++ .../yeelight/translations/zh-Hant.json | 39 +++++++++++++++++ 57 files changed, 1037 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/insteon/translations/pt.json create mode 100644 homeassistant/components/kodi/translations/pt.json create mode 100644 homeassistant/components/nzbget/translations/es.json create mode 100644 homeassistant/components/nzbget/translations/it.json create mode 100644 homeassistant/components/nzbget/translations/no.json create mode 100644 homeassistant/components/nzbget/translations/pt.json create mode 100644 homeassistant/components/progettihwsw/translations/ca.json create mode 100644 homeassistant/components/progettihwsw/translations/es.json create mode 100644 homeassistant/components/progettihwsw/translations/it.json create mode 100644 homeassistant/components/progettihwsw/translations/no.json create mode 100644 homeassistant/components/progettihwsw/translations/pt.json create mode 100644 homeassistant/components/progettihwsw/translations/ru.json create mode 100644 homeassistant/components/progettihwsw/translations/zh-Hant.json create mode 100644 homeassistant/components/risco/translations/pt.json create mode 100644 homeassistant/components/sharkiq/translations/ca.json create mode 100644 homeassistant/components/sharkiq/translations/es.json create mode 100644 homeassistant/components/sharkiq/translations/it.json create mode 100644 homeassistant/components/sharkiq/translations/no.json create mode 100644 homeassistant/components/sharkiq/translations/pt.json create mode 100644 homeassistant/components/sharkiq/translations/zh-Hant.json create mode 100644 homeassistant/components/shelly/translations/pt.json create mode 100644 homeassistant/components/spotify/translations/pt.json create mode 100644 homeassistant/components/wilight/translations/pt.json create mode 100644 homeassistant/components/yeelight/translations/it.json create mode 100644 homeassistant/components/yeelight/translations/ru.json create mode 100644 homeassistant/components/yeelight/translations/zh-Hant.json diff --git a/homeassistant/components/ambiclimate/translations/ru.json b/homeassistant/components/ambiclimate/translations/ru.json index 5712c99df22..7a440f28bed 100644 --- a/homeassistant/components/ambiclimate/translations/ru.json +++ b/homeassistant/components/ambiclimate/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", - "already_setup": "\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.", + "already_setup": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "no_config": "\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 Ambiclimate \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/ambiclimate/)." }, "create_entry": { diff --git a/homeassistant/components/august/translations/ru.json b/homeassistant/components/august/translations/ru.json index 5cc039b8e9e..9a49caed547 100644 --- a/homeassistant/components/august/translations/ru.json +++ b/homeassistant/components/august/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", diff --git a/homeassistant/components/avri/translations/ru.json b/homeassistant/components/avri/translations/ru.json index 6685a4cda47..ff8d8c12606 100644 --- a/homeassistant/components/avri/translations/ru.json +++ b/homeassistant/components/avri/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "invalid_country_code": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u0434\u0432\u0443\u0445\u0431\u0443\u043a\u0432\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b.", diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index 7a8f9e1b5c4..fdb25c2ee4d 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -1,7 +1,7 @@ { "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.", + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "no_devices": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "reauth_successful": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, diff --git a/homeassistant/components/azure_devops/translations/ru.json b/homeassistant/components/azure_devops/translations/ru.json index 2a7956c5c90..c98eddf5ade 100644 --- a/homeassistant/components/azure_devops/translations/ru.json +++ b/homeassistant/components/azure_devops/translations/ru.json @@ -1,7 +1,7 @@ { "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.", + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "reauth_successful": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { diff --git a/homeassistant/components/dexcom/translations/ru.json b/homeassistant/components/dexcom/translations/ru.json index 85e381be8c8..01bd9a3f0b3 100644 --- a/homeassistant/components/dexcom/translations/ru.json +++ b/homeassistant/components/dexcom/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_account": "\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." + "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "account_error": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", diff --git a/homeassistant/components/flick_electric/translations/ru.json b/homeassistant/components/flick_electric/translations/ru.json index c8ea23b3121..1f88596bfc0 100644 --- a/homeassistant/components/flick_electric/translations/ru.json +++ b/homeassistant/components/flick_electric/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", diff --git a/homeassistant/components/flume/translations/ru.json b/homeassistant/components/flume/translations/ru.json index eb9b1261bd9..0ed6e839227 100644 --- a/homeassistant/components/flume/translations/ru.json +++ b/homeassistant/components/flume/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", diff --git a/homeassistant/components/garmin_connect/translations/ru.json b/homeassistant/components/garmin_connect/translations/ru.json index b3aa4e6604c..c9448e1a3fc 100644 --- a/homeassistant/components/garmin_connect/translations/ru.json +++ b/homeassistant/components/garmin_connect/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json index c8949bdc16c..9066961ce29 100644 --- a/homeassistant/components/icloud/translations/ru.json +++ b/homeassistant/components/icloud/translations/ru.json @@ -1,7 +1,7 @@ { "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.", + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "no_device": "\u041d\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043c \u0438\u0437 \u0412\u0430\u0448\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \"\u041d\u0430\u0439\u0442\u0438 iPhone\"." }, "error": { diff --git a/homeassistant/components/insteon/translations/ca.json b/homeassistant/components/insteon/translations/ca.json index 859ba47b79e..8dd76ba29b8 100644 --- a/homeassistant/components/insteon/translations/ca.json +++ b/homeassistant/components/insteon/translations/ca.json @@ -27,6 +27,24 @@ "description": "Configura l'Insteon Hub versi\u00f3 2.", "title": "Insteon Hub versi\u00f3 2" }, + "hubv1": { + "data": { + "host": "Adre\u00e7a IP", + "port": "Port" + }, + "description": "Configura l'Insteon Hub versi\u00f3 1 (anterior a 2014).", + "title": "Insteon Hub versi\u00f3 1" + }, + "hubv2": { + "data": { + "host": "Adre\u00e7a IP", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "description": "Configura l'Insteon Hub versi\u00f3 2.", + "title": "Insteon Hub versi\u00f3 2" + }, "init": { "data": { "hubv1": "Hub versi\u00f3 1 (anterior a 2014)", @@ -42,6 +60,13 @@ }, "description": "Configura el m\u00f2dem Insteon PowerLink (PLM).", "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "Tipus de m\u00f2dem." + }, + "description": "Selecciona el tipus de m\u00f2dem Insteon.", + "title": "Insteon" } } }, diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 9bdec0e7fcc..bf38bb38d94 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -27,6 +27,24 @@ "description": "Configure el Insteon Hub versi\u00f3n 2.", "title": "Insteon Hub Versi\u00f3n 2" }, + "hubv1": { + "data": { + "host": "Direcci\u00f3n IP", + "port": "Puerto" + }, + "description": "Configure el Insteon Hub Versi\u00f3n 1 (anterior a 2014).", + "title": "Insteon Hub Versi\u00f3n 1" + }, + "hubv2": { + "data": { + "host": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Usuario" + }, + "description": "Configure el Insteon Hub versi\u00f3n 2.", + "title": "Insteon Hub Versi\u00f3n 2" + }, "init": { "data": { "hubv1": "Hub versi\u00f3n 1 (anterior a 2014)", @@ -42,6 +60,13 @@ }, "description": "Configure el M\u00f3dem Insteon PowerLink (PLM).", "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "Tipo de m\u00f3dem." + }, + "description": "Seleccione el tipo de m\u00f3dem Insteon.", + "title": "Insteon" } } }, diff --git a/homeassistant/components/insteon/translations/it.json b/homeassistant/components/insteon/translations/it.json index 4cafb5bf711..ab9902ec0ca 100644 --- a/homeassistant/components/insteon/translations/it.json +++ b/homeassistant/components/insteon/translations/it.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "Una connessione modem Insteon \u00e8 gi\u00e0 configurata", - "cannot_connect": "Impossibile connettersi al modem Insteon" + "cannot_connect": "Impossibile connettersi", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { - "cannot_connect": "Impossibile connettersi al modem Insteon, riprovare.", + "cannot_connect": "Impossibile connettersi", "select_single": "Selezionare un'opzione." }, "step": { @@ -27,6 +28,24 @@ "description": "Configurare la versione 2 di Insteon Hub.", "title": "Insteon Hub versione 2" }, + "hubv1": { + "data": { + "host": "Indirizzo IP", + "port": "Porta" + }, + "description": "Configurare la versione 1 di Insteon Hub (precedente al 2014).", + "title": "Insteon Hub Versione 1" + }, + "hubv2": { + "data": { + "host": "Indirizzo IP", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "description": "Configurare la versione 2 di Insteon Hub.", + "title": "Insteon Hub Versione 2" + }, "init": { "data": { "hubv1": "Hub versione 1 (precedente al 2014)", @@ -38,10 +57,17 @@ }, "plm": { "data": { - "device": "Dispositivo PLM (ad esempio /dev/ttyUSB0 o COM3)" + "device": "Percorso del dispositivo USB" }, "description": "Configurare Insteon PowerLink Modem (PLM).", "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "Tipo di modem." + }, + "description": "Seleziona il tipo di modem Insteon.", + "title": "Insteon" } } }, @@ -51,7 +77,7 @@ "cannot_connect": "Impossibile connettersi al modem Insteon" }, "error": { - "cannot_connect": "Impossibile connettersi al modem Insteon, riprovare.", + "cannot_connect": "Impossibile connettersi", "input_error": "Voci non valide, si prega di controllare i valori.", "select_single": "Selezionare un'opzione." }, @@ -77,10 +103,10 @@ }, "change_hub_config": { "data": { - "host": "Nuovo nome host o indirizzo IP", - "password": "Nuova password", - "port": "Nuovo numero di porta", - "username": "Nuovo nome utente" + "host": "Indirizzo IP", + "password": "Password", + "port": "Porta", + "username": "Nome utente" }, "description": "Modificare le informazioni di connessione di Insteon Hub. \u00c8 necessario riavviare Home Assistant dopo aver apportato questa modifica. Ci\u00f2 non modifica la configurazione dell'Hub stesso. Per modificare la configurazione nell'Hub, utilizzare l'app Hub.", "title": "Insteon" diff --git a/homeassistant/components/insteon/translations/no.json b/homeassistant/components/insteon/translations/no.json index e0ff69823e4..4dfaa95a20e 100644 --- a/homeassistant/components/insteon/translations/no.json +++ b/homeassistant/components/insteon/translations/no.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "En Insteon-modemtilkobling er allerede konfigurert", - "cannot_connect": "Kan ikke koble til Insteon-modemet" + "cannot_connect": "Tilkobling mislyktes." }, "error": { - "cannot_connect": "Kan ikke koble til Insteon-modemet, pr\u00f8v p\u00e5 nytt.", + "cannot_connect": "Tilkobling mislyktes.", "select_single": "Velg ett alternativ." }, "step": { @@ -27,6 +27,24 @@ "description": "Konfigurer Insteon Hub versjon 2.", "title": "Insteon Hub versjon 2" }, + "hubv1": { + "data": { + "host": "IP adresse", + "port": "Port" + }, + "description": "Konfigurer Insteon Hub versjon 1 (f\u00f8r 2014).", + "title": "Insteon Hub versjon 1" + }, + "hubv2": { + "data": { + "host": "IP adresse", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "description": "Konfigurer Insteon Hub versjon 2.", + "title": "Insteon Hub versjon 2" + }, "init": { "data": { "hubv1": "Hub versjon 1 (f\u00f8r 2014)", @@ -38,10 +56,17 @@ }, "plm": { "data": { - "device": "PLM-enhet (dvs. /dev/ttyUSB0 eller COM3)" + "device": "USB enhetsbane" }, "description": "Konfigurer Insteon PowerLink-modem (PLM).", "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "Modemtype." + }, + "description": "Velg Insteon modemtype.", + "title": "Insteon" } } }, @@ -51,7 +76,7 @@ "cannot_connect": "Kan ikke koble til Insteon-modemet" }, "error": { - "cannot_connect": "Kan ikke koble til Insteon-modemet, pr\u00f8v p\u00e5 nytt.", + "cannot_connect": "Tilkobling mislyktes.", "input_error": "Ugyldige oppf\u00f8ringer, vennligst sjekk verdiene dine.", "select_single": "Velg ett alternativ." }, @@ -77,10 +102,10 @@ }, "change_hub_config": { "data": { - "host": "Nytt vertsnavn eller IP-adresse", - "password": "Nytt passord", - "port": "Nytt portnummer", - "username": "Nytt brukernavn" + "host": "IP adresse", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" }, "description": "Endre Insteon Hub-tilkoblingsinformasjonen. Du m\u00e5 starte Home Assistant p\u00e5 nytt n\u00e5r du har gjort denne endringen. Dette endrer ikke konfigurasjonen av selve huben. For \u00e5 endre konfigurasjonen i huben bruker du hub-appen.", "title": "Insteon" diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json new file mode 100644 index 00000000000..be84de305ce --- /dev/null +++ b/homeassistant/components/insteon/translations/pt.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" + }, + "step": { + "hubv1": { + "data": { + "host": "Endere\u00e7o IP", + "port": "Porta" + }, + "description": "Configure o Insteon Hub Vers\u00e3o 1 (pr\u00e9-2014).", + "title": "Insteon Hub vers\u00e3o 1" + }, + "hubv2": { + "data": { + "host": "Endere\u00e7o IP", + "password": "Senha", + "port": "Porta", + "username": "Utilizador" + }, + "description": "Configure o Insteon Hub Vers\u00e3o 2.", + "title": "Insteon Hub vers\u00e3o 2" + }, + "user": { + "data": { + "modem_type": "Tipo de modem." + }, + "description": "Selecione o tipo de modem Insteon.", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/ru.json b/homeassistant/components/insteon/translations/ru.json index 620bcdc9154..65861a0a161 100644 --- a/homeassistant/components/insteon/translations/ru.json +++ b/homeassistant/components/insteon/translations/ru.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "\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.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "select_single": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u043f\u0446\u0438\u044e." }, "step": { @@ -27,6 +28,24 @@ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Insteon Hub \u0432\u0435\u0440\u0441\u0438\u0438 2", "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0438\u044f 2" }, + "hubv1": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Insteon Hub \u0432\u0435\u0440\u0441\u0438\u0438 1 (\u0434\u043e 2014 \u0433\u043e\u0434\u0430)", + "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0438\u044f 1" + }, + "hubv2": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Insteon Hub \u0432\u0435\u0440\u0441\u0438\u0438 2", + "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0438\u044f 2" + }, "init": { "data": { "hubv1": "\u0412\u0435\u0440\u0441\u0438\u044f 1 (\u0434\u043e 2014 \u0433\u043e\u0434\u0430)", @@ -38,10 +57,17 @@ }, "plm": { "data": { - "device": "PLM-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: /dev/ttyUSB0 \u0438\u043b\u0438 COM3)" + "device": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u043e\u0434\u0435\u043c\u0430 Insteon PowerLink (PLM)", "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "\u0422\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0430 Insteon.", + "title": "Insteon" } } }, @@ -51,7 +77,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "input_error": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f.", "select_single": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u043f\u0446\u0438\u044e." }, @@ -77,10 +103,10 @@ }, "change_hub_config": { "data": { - "host": "\u041d\u043e\u0432\u043e\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", - "password": "\u041d\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c", - "port": "\u041d\u043e\u0432\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430", - "username": "\u041d\u043e\u0432\u044b\u0439 \u043b\u043e\u0433\u0438\u043d" + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" }, "description": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Insteon Hub. \u041f\u043e\u0441\u043b\u0435 \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Home Assistant. \u042d\u0442\u043e \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0427\u0442\u043e\u0431\u044b \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Hub.", "title": "Insteon" diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index 910377da4d2..c9811c47770 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "Insteon \u6578\u64da\u6a5f\u9023\u7dda\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Insteon \u6578\u64da\u6a5f" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" }, "error": { - "cannot_connect": "Insteon \u6578\u64da\u6a5f\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "select_single": "\u9078\u64c7\u9078\u9805\u3002" }, "step": { @@ -27,6 +28,24 @@ "description": "\u8a2d\u5b9a Insteon Hub \u7b2c 2 \u7248\u3002", "title": "Insteon Hub \u7b2c 2 \u7248" }, + "hubv1": { + "data": { + "host": "IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a Insteon Hub \u7b2c 1 \u7248\uff082014 \u5e74\u4ee5\u524d\uff09\u3002", + "title": "Insteon Hub \u7b2c 1 \u7248" + }, + "hubv2": { + "data": { + "host": "IP \u4f4d\u5740", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a Insteon Hub \u7b2c 2 \u7248\u3002", + "title": "Insteon Hub \u7b2c 2 \u7248" + }, "init": { "data": { "hubv1": "Insteon Hub \u7b2c 1 \u7248\uff082014 \u5e74\u4ee5\u524d\uff09", @@ -38,10 +57,17 @@ }, "plm": { "data": { - "device": "PLM \u8a2d\u5099\uff08\u4f8b\u5982 /dev/ttyUSB0 \u6216 COM3\uff09" + "device": "USB \u8a2d\u5099\u8def\u5f91" }, "description": "\u8a2d\u5b9a PowerLink Modem (PLM)\u3002", "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "\u6578\u64da\u6a5f\u985e\u578b\u3002" + }, + "description": "\u9078\u64c7 Insteon \u6578\u64da\u6a5f\u985e\u578b\u3002", + "title": "Insteon" } } }, @@ -51,7 +77,7 @@ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Insteon \u6578\u64da\u6a5f\u3002" }, "error": { - "cannot_connect": "Insteon \u6578\u64da\u6a5f\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "input_error": "\u7269\u4ef6\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u503c\u3002", "select_single": "\u9078\u64c7\u9078\u9805\u3002" }, @@ -77,10 +103,10 @@ }, "change_hub_config": { "data": { - "host": "\u65b0\u589e\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", - "password": "\u65b0\u589e\u5bc6\u78bc", - "port": "\u65b0\u589e\u901a\u8a0a\u57e0\u865f", - "username": "\u65b0\u589e\u4f7f\u7528\u8005\u540d\u7a31" + "host": "IP \u4f4d\u5740", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8b8a\u66f4 Insteon Hub \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 Hub \u8a2d\u5099\u672c\u8eab\u7684\u8a2d\u5b9a\uff0c\u5982\u6b32\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3001\u5247\u8acb\u4f7f\u7528 Hub app\u3002", "title": "Insteon" diff --git a/homeassistant/components/juicenet/translations/ru.json b/homeassistant/components/juicenet/translations/ru.json index 3bb4084bac3..69b68f82990 100644 --- a/homeassistant/components/juicenet/translations/ru.json +++ b/homeassistant/components/juicenet/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", diff --git a/homeassistant/components/kodi/translations/pt.json b/homeassistant/components/kodi/translations/pt.json new file mode 100644 index 00000000000..25e47d0be96 --- /dev/null +++ b/homeassistant/components/kodi/translations/pt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Informa\u00e7\u00f5es de conex\u00e3o Kodi. Certifique-se de habilitar \"Permitir controle do Kodi via HTTP\" em Sistema / Configura\u00e7\u00f5es / Rede / Servi\u00e7os." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ca.json b/homeassistant/components/konnected/translations/ca.json index 8938db2621b..2e5ce9b3c98 100644 --- a/homeassistant/components/konnected/translations/ca.json +++ b/homeassistant/components/konnected/translations/ca.json @@ -32,7 +32,9 @@ "not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io" }, "error": { - "bad_host": "L'URL de sustituci\u00f3 de l'amfitri\u00f3 de l'API \u00e9s inv\u00e0lid" + "bad_host": "L'URL de sustituci\u00f3 de l'amfitri\u00f3 de l'API \u00e9s inv\u00e0lid", + "one": "Un", + "other": "Altres" }, "step": { "options_binary": { diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index d95bb809cd5..685aee2f59c 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "user_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." + "user_already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." @@ -11,7 +11,7 @@ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360.", - "user_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." + "user_already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "step": { "user": { diff --git a/homeassistant/components/mill/translations/ru.json b/homeassistant/components/mill/translations/ru.json index cdc89033330..bf836c6014e 100644 --- a/homeassistant/components/mill/translations/ru.json +++ b/homeassistant/components/mill/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." diff --git a/homeassistant/components/nexia/translations/ru.json b/homeassistant/components/nexia/translations/ru.json index ee8fc183b3e..1dd3be26fd1 100644 --- a/homeassistant/components/nexia/translations/ru.json +++ b/homeassistant/components/nexia/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", diff --git a/homeassistant/components/nzbget/translations/es.json b/homeassistant/components/nzbget/translations/es.json new file mode 100644 index 00000000000..aeb78ca7886 --- /dev/null +++ b/homeassistant/components/nzbget/translations/es.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n.", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "ssl": "NZBGet utiliza un certificado SSL", + "username": "Usuario", + "verify_ssl": "NZBGet utiliza un certificado adecuado" + }, + "title": "Conectarse a NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/it.json b/homeassistant/components/nzbget/translations/it.json new file mode 100644 index 00000000000..31438f9d55b --- /dev/null +++ b/homeassistant/components/nzbget/translations/it.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta", + "ssl": "NZBGet utilizza un certificato SSL", + "username": "Nome utente", + "verify_ssl": "NZBGet utilizza un certificato proprio" + }, + "title": "Connettiti a NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequenza di aggiornamento (secondi)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/no.json b/homeassistant/components/nzbget/translations/no.json new file mode 100644 index 00000000000..e8230da6f2d --- /dev/null +++ b/homeassistant/components/nzbget/translations/no.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes.", + "invalid_auth": "Ugyldig godkjenning" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "ssl": "NZBGet bruker et SSL-sertifikat", + "username": "Brukernavn", + "verify_ssl": "NZBGet bruker et riktig sertifikat" + }, + "title": "Koble til NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdater frekvens (sekunder)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/pt.json b/homeassistant/components/nzbget/translations/pt.json new file mode 100644 index 00000000000..93aa814f47a --- /dev/null +++ b/homeassistant/components/nzbget/translations/pt.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 se encontra configurado. S\u00f3 \u00e9 permitida uma configura\u00e7\u00e3o.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Erro ao conectar-se", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome", + "password": "Senha", + "port": "Porta", + "ssl": "NZBGet usa um certificado SSL", + "username": "Utilizador", + "verify_ssl": "NZBGet usa um certificado adequado" + }, + "title": "Conectar ao NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json index 83d0e16f4c4..24e9cf1865a 100644 --- a/homeassistant/components/ovo_energy/translations/ru.json +++ b/homeassistant/components/ovo_energy/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "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.", + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "authorization_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, diff --git a/homeassistant/components/plum_lightpad/translations/ru.json b/homeassistant/components/plum_lightpad/translations/ru.json index 3e14039ae00..c17eb37b151 100644 --- a/homeassistant/components/plum_lightpad/translations/ru.json +++ b/homeassistant/components/plum_lightpad/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." diff --git a/homeassistant/components/progettihwsw/translations/ca.json b/homeassistant/components/progettihwsw/translations/ca.json new file mode 100644 index 00000000000..010de0dc876 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/ca.json @@ -0,0 +1,39 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat", + "wrong_info_relay_modes": "La selecci\u00f3 del mode de rel\u00e9 ha de ser monostable o biestable." + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Rel\u00e9 1", + "relay_10": "Rel\u00e9 10", + "relay_11": "Rel\u00e9 11", + "relay_12": "Rel\u00e9 12", + "relay_13": "Relleu 13", + "relay_14": "Rel\u00e9 14", + "relay_15": "Rel\u00e9 15", + "relay_16": "Rel\u00e9 16", + "relay_2": "Rel\u00e9 2", + "relay_3": "Rel\u00e9 3", + "relay_4": "Rel\u00e9 4", + "relay_5": "Rel\u00e9 5", + "relay_6": "Rel\u00e9 6", + "relay_7": "Rel\u00e9 7", + "relay_8": "Rel\u00e9 8", + "relay_9": "Rel\u00e9 9" + }, + "title": "Configurar rel\u00e9s" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "title": "Configuraci\u00f3 del tauler" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/es.json b/homeassistant/components/progettihwsw/translations/es.json new file mode 100644 index 00000000000..3da2bc22c45 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/es.json @@ -0,0 +1,39 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado", + "wrong_info_relay_modes": "La selecci\u00f3n del modo de rel\u00e9 debe ser monoestable o biestable." + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Rel\u00e9 1", + "relay_10": "Rel\u00e9 10", + "relay_11": "Rel\u00e9 11", + "relay_12": "Rel\u00e9 12", + "relay_13": "Rel\u00e9 13", + "relay_14": "Rel\u00e9 14", + "relay_15": "Rel\u00e9 15", + "relay_16": "Rel\u00e9 16", + "relay_2": "Rel\u00e9 2", + "relay_3": "Rel\u00e9 3", + "relay_4": "Rel\u00e9 4", + "relay_5": "Rel\u00e9 5", + "relay_6": "Rel\u00e9 6", + "relay_7": "Rel\u00e9 7", + "relay_8": "Rel\u00e9 8", + "relay_9": "Rel\u00e9 9" + }, + "title": "Configurar rel\u00e9s" + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Configurar tablero" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/it.json b/homeassistant/components/progettihwsw/translations/it.json new file mode 100644 index 00000000000..66c40a5a5a7 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto", + "wrong_info_relay_modes": "La selezione del modo di funzionamento del rel\u00e8 deve essere monostabile o bistabile." + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Rel\u00e8 1", + "relay_10": "Rel\u00e8 10", + "relay_11": "Rel\u00e8 11", + "relay_12": "Rel\u00e8 12", + "relay_13": "Rel\u00e8 13", + "relay_14": "Rel\u00e8 14", + "relay_15": "Rel\u00e8 15", + "relay_16": "Rel\u00e8 16", + "relay_2": "Rel\u00e8 2", + "relay_3": "Rel\u00e8 3", + "relay_4": "Rel\u00e8 4", + "relay_5": "Rel\u00e8 5", + "relay_6": "Rel\u00e8 6", + "relay_7": "Rel\u00e8 7", + "relay_8": "Rel\u00e8 8", + "relay_9": "Rel\u00e8 9" + }, + "title": "Configurare i rel\u00e8" + }, + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "title": "Impostare la scheda" + } + } + }, + "title": "Automazione di ProgettiHWSW" +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/no.json b/homeassistant/components/progettihwsw/translations/no.json new file mode 100644 index 00000000000..66b8348f759 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes.", + "unknown": "Uventet feil", + "wrong_info_relay_modes": "Valg av rel\u00e9modus m\u00e5 v\u00e6re monostable eller bistable." + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Rele 1", + "relay_10": "Rele 10", + "relay_11": "Rele 11", + "relay_12": "Rele 12", + "relay_13": "Rele 13", + "relay_14": "Rele 14", + "relay_15": "Rele 15", + "relay_16": "Rele 16", + "relay_2": "Rele 2", + "relay_3": "Rele 3", + "relay_4": "Rele 4", + "relay_5": "Rele 5", + "relay_6": "Rele 6", + "relay_7": "Rele 7", + "relay_8": "Rele 8", + "relay_9": "Rele 9 " + }, + "title": "Sett opp rel\u00e9er" + }, + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "title": "Definere tavle" + } + } + }, + "title": "ProgettiHWSW automatisering" +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/pt.json b/homeassistant/components/progettihwsw/translations/pt.json new file mode 100644 index 00000000000..5fac50b2cc1 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/pt.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "cannot_connect": "Erro ao conectar-se", + "unknown": "Erro inesperado", + "wrong_info_relay_modes": "A sele\u00e7\u00e3o do modo de rel\u00e9 deve ser monoest\u00e1vel ou biest\u00e1vel." + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Rel\u00e9 1", + "relay_10": "Rel\u00e9 10", + "relay_11": "Rel\u00e9 11", + "relay_12": "Rel\u00e9 12", + "relay_13": "Rel\u00e9 13", + "relay_14": "Rel\u00e9 14", + "relay_15": "Rel\u00e9 15", + "relay_16": "Rel\u00e9 16", + "relay_2": "Rel\u00e9 2", + "relay_3": "Rel\u00e9 3", + "relay_4": "Rel\u00e9 4", + "relay_5": "Rel\u00e9 5", + "relay_6": "Rel\u00e9 6", + "relay_7": "Rel\u00e9 7", + "relay_8": "Rel\u00e9 8", + "relay_9": "Rel\u00e9 9" + }, + "title": "Configurar rel\u00e9s" + }, + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + }, + "title": "Configurar placa" + } + } + }, + "title": "Automa\u00e7\u00e3o ProgettiHWSW" +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/ru.json b/homeassistant/components/progettihwsw/translations/ru.json new file mode 100644 index 00000000000..11ecb8bfc33 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/ru.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "wrong_info_relay_modes": "\u0420\u0435\u0436\u0438\u043c \u0440\u0435\u043b\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \"monostable\" \u0438\u043b\u0438 \"bistable\"." + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "\u0420\u0435\u043b\u0435 1", + "relay_10": "\u0420\u0435\u043b\u0435 10", + "relay_11": "\u0420\u0435\u043b\u0435 11", + "relay_12": "\u0420\u0435\u043b\u0435 12", + "relay_13": "\u0420\u0435\u043b\u0435 13", + "relay_14": "\u0420\u0435\u043b\u0435 14", + "relay_15": "\u0420\u0435\u043b\u0435 15", + "relay_16": "\u0420\u0435\u043b\u0435 16", + "relay_2": "\u0420\u0435\u043b\u0435 2", + "relay_3": "\u0420\u0435\u043b\u0435 3", + "relay_4": "\u0420\u0435\u043b\u0435 4", + "relay_5": "\u0420\u0435\u043b\u0435 5", + "relay_6": "\u0420\u0435\u043b\u0435 6", + "relay_7": "\u0420\u0435\u043b\u0435 7", + "relay_8": "\u0420\u0435\u043b\u0435 8", + "relay_9": "\u0420\u0435\u043b\u0435 9" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0440\u0435\u043b\u0435" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u043b\u0430\u0442\u044b" + } + } + }, + "title": "ProgettiHWSW Automation" +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/zh-Hant.json b/homeassistant/components/progettihwsw/translations/zh-Hant.json new file mode 100644 index 00000000000..81185a15e2a --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "wrong_info_relay_modes": "\u7e7c\u96fb\u5668\u6a21\u5f0f\u9078\u64c7\u5fc5\u9808\u70ba\u8907\u632f\u5668\u6216\u96d9\u7a69\u614b\u96fb\u8def\u3002" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "\u7e7c\u96fb\u5668 1", + "relay_10": "\u7e7c\u96fb\u5668 10", + "relay_11": "\u7e7c\u96fb\u5668 11", + "relay_12": "\u7e7c\u96fb\u5668 12", + "relay_13": "\u7e7c\u96fb\u5668 13", + "relay_14": "\u7e7c\u96fb\u5668 14", + "relay_15": "\u7e7c\u96fb\u5668 15", + "relay_16": "\u7e7c\u96fb\u5668 16", + "relay_2": "\u7e7c\u96fb\u5668 2", + "relay_3": "\u7e7c\u96fb\u5668 3", + "relay_4": "\u7e7c\u96fb\u5668 4", + "relay_5": "\u7e7c\u96fb\u5668 5", + "relay_6": "\u7e7c\u96fb\u5668 6", + "relay_7": "\u7e7c\u96fb\u5668 7", + "relay_8": "\u7e7c\u96fb\u5668 8", + "relay_9": "\u7e7c\u96fb\u5668 9" + }, + "title": "\u8a2d\u5b9a\u7e7c\u96fb\u5668" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "title": "\u8a2d\u5b9a\u4e3b\u6a5f\u677f" + } + } + }, + "title": "ProgettiHWSW \u81ea\u52d5\u5316" +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json index c308cddeb76..a0968f4aa50 100644 --- a/homeassistant/components/risco/translations/es.json +++ b/homeassistant/components/risco/translations/es.json @@ -27,6 +27,15 @@ "scan_interval": "Con qu\u00e9 frecuencia sondear Risco (en segundos)" }, "title": "Configurar opciones" + }, + "risco_to_ha": { + "data": { + "A": "Grupo A", + "B": "Grupo B", + "C": "Grupo C", + "D": "Grupo D" + }, + "title": "Asignar estados de Risco a estados de Home Assistant" } } } diff --git a/homeassistant/components/risco/translations/it.json b/homeassistant/components/risco/translations/it.json index 9edc795f4e9..2bfccc2b9c9 100644 --- a/homeassistant/components/risco/translations/it.json +++ b/homeassistant/components/risco/translations/it.json @@ -20,6 +20,16 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "Attivo Fuori Casa", + "armed_custom_bypass": "Attivo con Bypass Personalizzato", + "armed_home": "Attivo In Casa", + "armed_night": "Attivo Notte" + }, + "description": "Selezionare lo stato in cui impostare l'allarme Risco quando si attiva l'allarme Home Assistant", + "title": "Mappare gli stati di Home Assistant negli stati di Risco" + }, "init": { "data": { "code_arm_required": "Richiedi codice PIN per armare", @@ -27,6 +37,18 @@ "scan_interval": "Con che frequenza interrogare Risco (in secondi)" }, "title": "Configura le opzioni" + }, + "risco_to_ha": { + "data": { + "A": "Gruppo A", + "B": "Gruppo B", + "C": "Gruppo C", + "D": "Gruppo D", + "arm": "Attivo (Fuori Casa)", + "partial_arm": "Parzialmente attivo (IN CASA)" + }, + "description": "Seleziona lo stato che verr\u00e0 segnalato dall'allarme Home Assistant per ogni stato segnalato da Risco", + "title": "Mappare gli stati di Risco agli stati di Home Assistant" } } } diff --git a/homeassistant/components/risco/translations/pt.json b/homeassistant/components/risco/translations/pt.json new file mode 100644 index 00000000000..02f17e897ec --- /dev/null +++ b/homeassistant/components/risco/translations/pt.json @@ -0,0 +1,32 @@ +{ + "options": { + "step": { + "ha_to_risco": { + "data": { + "armed_away": "Armado Ausente", + "armed_custom_bypass": "Armado - Desativa\u00e7\u00e3o personalizada", + "armed_home": "Armado", + "armed_night": "Armado modo Noite" + } + }, + "init": { + "data": { + "code_arm_required": "Exigir c\u00f3digo PIN para armar", + "code_disarm_required": "Exigir c\u00f3digo PIN para desarmar" + } + }, + "risco_to_ha": { + "data": { + "A": "grupo A", + "B": "Grupo B", + "C": "Grupo C", + "D": "Grupo D", + "arm": "Armado (AUSENTE)", + "partial_arm": "Parcialmente armado (PRESENTE)" + }, + "description": "Selecione o estado que o alarme do Home Assistent ir\u00e1 relatar para cada estado relatado pela Risco", + "title": "Associa\u00e7\u00e3o de estados do Mapa Risco e do Home Assistant" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/ca.json b/homeassistant/components/sharkiq/translations/ca.json new file mode 100644 index 00000000000..c7a7c13e047 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured_account": "El compte ja ha estat configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/es.json b/homeassistant/components/sharkiq/translations/es.json new file mode 100644 index 00000000000..70179f4a8e1 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured_account": "La cuenta ya ha sido configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/it.json b/homeassistant/components/sharkiq/translations/it.json new file mode 100644 index 00000000000..a79031ecbc0 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured_account": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/no.json b/homeassistant/components/sharkiq/translations/no.json new file mode 100644 index 00000000000..0486219df39 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured_account": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes.", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/pt.json b/homeassistant/components/sharkiq/translations/pt.json new file mode 100644 index 00000000000..3469901540c --- /dev/null +++ b/homeassistant/components/sharkiq/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured_account": "Conta j\u00e1 se encontra configurada" + }, + "error": { + "cannot_connect": "Erro ao conectar-se", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/ru.json b/homeassistant/components/sharkiq/translations/ru.json index 0c8d369dbec..a2ac3f08f36 100644 --- a/homeassistant/components/sharkiq/translations/ru.json +++ b/homeassistant/components/sharkiq/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_account": "\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." + "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/sharkiq/translations/zh-Hant.json b/homeassistant/components/sharkiq/translations/zh-Hant.json new file mode 100644 index 00000000000..42d245709c2 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pt.json b/homeassistant/components/shelly/translations/pt.json new file mode 100644 index 00000000000..30ebb24a047 --- /dev/null +++ b/homeassistant/components/shelly/translations/pt.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 se encontra configurado" + }, + "error": { + "auth_not_supported": "Dispositivos Shelly que requerem autentica\u00e7\u00e3o n\u00e3o s\u00e3o atualmente suportados.", + "cannot_connect": "Erro ao conectar-se", + "unknown": "Erro inesperado" + }, + "flow_title": "Shelly: {name}", + "step": { + "confirm_discovery": { + "description": "Deseja configurar o {model} em {host} ?" + }, + "user": { + "data": { + "host": "Servidor" + } + } + } + }, + "title": "Shelly" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index cd539ae184c..03e4cfdcf24 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -1,7 +1,7 @@ { "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.", + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "reauth_successful": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index c3bfaa47f32..bb494f27860 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\u00c8 possibile configurare un solo account di Spotify.", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione", - "missing_configuration": "L'integrazione di Spotify non \u00e8 configurata. Si prega di seguire la documentazione." + "missing_configuration": "L'integrazione di Spotify non \u00e8 configurata. Si prega di seguire la documentazione.", + "reauth_account_mismatch": "L'account Spotify con cui si \u00e8 autenticati non corrisponde all'account necessario per la ri-autenticazione." }, "create_entry": { "default": "Autenticato con successo con Spotify." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione di Spotify deve essere nuovamente autenticata con Spotify per l'account: {account}", + "title": "Eseguire nuovamente l'autenticazione con Spotify" } } } diff --git a/homeassistant/components/spotify/translations/pt.json b/homeassistant/components/spotify/translations/pt.json new file mode 100644 index 00000000000..b459d4e6bfd --- /dev/null +++ b/homeassistant/components/spotify/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "reauth_account_mismatch": "A conta Spotify com a qual foi autenticada n\u00e3o corresponde \u00e0 conta necess\u00e1ria para a reautentica\u00e7\u00e3o." + }, + "step": { + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Spotify precisa ser reautenticada com o Spotify para a conta: {account}", + "title": "Reautenticar com Spotify" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/ru.json b/homeassistant/components/tibber/translations/ru.json index 06a5bf1331f..715fcf1179a 100644 --- a/homeassistant/components/tibber/translations/ru.json +++ b/homeassistant/components/tibber/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index b2ccada9e63..a3de1ee4555 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -1,7 +1,7 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, "error": { "login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." diff --git a/homeassistant/components/wilight/translations/pt.json b/homeassistant/components/wilight/translations/pt.json new file mode 100644 index 00000000000..51aad9406f0 --- /dev/null +++ b/homeassistant/components/wilight/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 se encontra configurado", + "not_supported_device": "Este WiLight n\u00e3o \u00e9 compat\u00edvel atualmente", + "not_wilight_device": "Este dispositivo n\u00e3o \u00e9 WiLight" + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "Deseja configurar o WiLight {name} ? \n\n Suporta: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/en.json b/homeassistant/components/yeelight/translations/en.json index 68adafdf376..602ca9f1399 100644 --- a/homeassistant/components/yeelight/translations/en.json +++ b/homeassistant/components/yeelight/translations/en.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + "already_configured": "Device is already configured", + "no_devices_found": "No devices found on the network" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "Failed to connect" }, "step": { "pick_device": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "ip_address": "[%key:common::config_flow::data::ip%]" + "ip_address": "IP Address" }, "description": "If you leave IP address empty, discovery will be used to find devices." } diff --git a/homeassistant/components/yeelight/translations/it.json b/homeassistant/components/yeelight/translations/it.json new file mode 100644 index 00000000000..e3d965661b5 --- /dev/null +++ b/homeassistant/components/yeelight/translations/it.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "ip_address": "Indirizzo IP" + }, + "description": "Se lasci vuoto l'indirizzo IP, verr\u00e0 utilizzato il rilevamento per trovare i dispositivi." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Modello (opzionale)", + "nightlight_switch": "Usa l'interruttore luce notturna", + "save_on_change": "Salva stato su modifica", + "transition": "Tempo di transizione (ms)", + "use_music_mode": "Abilita la modalit\u00e0 musica" + }, + "description": "Se lasci il modello vuoto, verr\u00e0 rilevato automaticamente." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/ru.json b/homeassistant/components/yeelight/translations/ru.json new file mode 100644 index 00000000000..550d8a18472 --- /dev/null +++ b/homeassistant/components/yeelight/translations/ru.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0415\u0441\u043b\u0438 \u043d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "\u041c\u043e\u0434\u0435\u043b\u044c (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "nightlight_switch": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u0434\u043b\u044f \u043d\u043e\u0447\u043d\u0438\u043a\u0430", + "save_on_change": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438", + "transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 (\u0432 \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "use_music_mode": "\u041c\u0443\u0437\u044b\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c" + }, + "description": "\u0415\u0441\u043b\u0438 \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u0430, \u043e\u043d\u0430 \u0431\u0443\u0434\u0435\u0442 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json new file mode 100644 index 00000000000..4f5f766e3e2 --- /dev/null +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "pick_device": { + "data": { + "device": "\u8a2d\u5099" + } + }, + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740" + }, + "description": "\u5047\u5982 IP \u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u8a2d\u5099\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "\u578b\u865f\uff08\u9078\u9805\uff09", + "nightlight_switch": "\u4f7f\u7528\u591c\u71c8\u958b\u95dc", + "save_on_change": "\u65bc\u8b8a\u66f4\u6642\u5132\u5b58\u72c0\u614b", + "transition": "\u8f49\u63db\u6642\u9593\uff08\u6beb\u79d2\uff09", + "use_music_mode": "\u958b\u555f\u97f3\u6a02\u6a21\u5f0f" + }, + "description": "\u5047\u5982\u578b\u865f\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u81ea\u52d5\u5075\u6e2c\u578b\u865f\u3002" + } + } + }, + "title": "Yeelight" +} \ No newline at end of file From a77e09b2c2ad0f43fd67f199d9a729f710a4b7c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2020 19:07:40 -0500 Subject: [PATCH 538/862] Make async_track_template_result track multiple templates (#39371) * Make async_track_template_result track multiple templates Combine template entity updates to only write ha state once per template group update * Make async_track_template_result use dataclasses for input/output * black versions * naming --- .../components/bayesian/binary_sensor.py | 10 +- .../components/template/template_entity.py | 91 +++--- homeassistant/components/template/trigger.py | 13 +- .../components/universal/media_player.py | 11 +- .../components/websocket_api/commands.py | 12 +- homeassistant/helpers/event.py | 297 ++++++++++++------ homeassistant/helpers/template.py | 2 +- tests/helpers/test_event.py | 287 +++++++++++++---- 8 files changed, 508 insertions(+), 215 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 86f11cda7e1..8d4dab62263 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -21,6 +21,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( + TrackTemplate, async_track_state_change_event, async_track_template_result, ) @@ -187,7 +188,10 @@ class BayesianBinarySensor(BinarySensorEntity): ) @callback - def _async_template_result_changed(event, template, last_result, result): + def _async_template_result_changed(event, updates): + track_template_result = updates.pop() + template = track_template_result.template + result = track_template_result.result entity = event and event.data.get("entity_id") if isinstance(result, TemplateError): @@ -215,7 +219,9 @@ class BayesianBinarySensor(BinarySensorEntity): for template in self.observations_by_template: info = async_track_template_result( - self.hass, template, _async_template_result_changed + self.hass, + [TrackTemplate(template, None)], + _async_template_result_changed, ) self._callbacks.append(info) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index d63a2866510..20b0caec3ca 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -1,7 +1,7 @@ """TemplateEntity utility class.""" import logging -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, List, Optional, Union import voluptuous as vol @@ -9,7 +9,12 @@ from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import Event, async_track_template_result +from homeassistant.helpers.event import ( + Event, + TrackTemplate, + TrackTemplateResult, + async_track_template_result, +) from homeassistant.helpers.template import Template, result_as_boolean _LOGGER = logging.getLogger(__name__) @@ -34,7 +39,6 @@ class _TemplateAttribute: self.validator = validator self.on_update = on_update self.async_update = None - self.add_complete = False self.none_on_template_error = none_on_template_error @callback @@ -54,21 +58,14 @@ class _TemplateAttribute: setattr(self._entity, self._attribute, attr_result) @callback - def _write_update_if_added(self): - if self.add_complete: - self._entity.async_write_ha_state() - - @callback - def _handle_result( + def handle_result( self, event: Optional[Event], template: Template, - last_result: Optional[str], + last_result: Union[str, None, TemplateError], result: Union[str, TemplateError], ) -> None: - if event: - self._entity.async_set_context(event.context) - + """Handle a template result event callback.""" if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') " @@ -83,13 +80,10 @@ class _TemplateAttribute: self._default_update(result) else: self.on_update(result) - self._write_update_if_added() - return if not self.validator: self.on_update(result) - self._write_update_if_added() return try: @@ -107,26 +101,10 @@ class _TemplateAttribute: ex.msg, ) self.on_update(None) - self._write_update_if_added() return self.on_update(validated) - self._write_update_if_added() - - @callback - def async_template_startup(self) -> None: - """Call from containing entity when added to hass.""" - result_info = async_track_template_result( - self._entity.hass, self.template, self._handle_result - ) - - self.async_update = result_info.async_refresh - - @callback - def _remove_from_hass(): - result_info.async_remove() - - return _remove_from_hass + return class TemplateEntity(Entity): @@ -141,7 +119,8 @@ class TemplateEntity(Entity): attribute_templates=None, ): """Template Entity.""" - self._template_attrs = [] + self._template_attrs = {} + self._async_update = None self._attribute_templates = attribute_templates self._attributes = {} self._availability_template = availability_template @@ -233,17 +212,41 @@ class TemplateEntity(Entity): self, attribute, template, validator, on_update, none_on_template_error ) attribute.async_setup() - self._template_attrs.append(attribute) + self._template_attrs.setdefault(template, []) + self._template_attrs[template].append(attribute) + + @callback + def _handle_results( + self, + event: Optional[Event], + updates: List[TrackTemplateResult], + ) -> None: + """Call back the results to the attributes.""" + if event: + self.async_set_context(event.context) + + for update in updates: + for attr in self._template_attrs[update.template]: + attr.handle_result( + event, update.template, update.last_result, update.result + ) + + if self._async_update: + self.async_write_ha_state() async def _async_template_startup(self, *_) -> None: - # async_update will not write state - # until "add_complete" is set on the attribute - for attribute in self._template_attrs: - self.async_on_remove(attribute.async_template_startup()) - await self.async_update() - for attribute in self._template_attrs: - attribute.add_complete = True + # _handle_results will not write state until "_async_update" is set + template_var_tups = [ + TrackTemplate(template, None) for template in self._template_attrs + ] + + result_info = async_track_template_result( + self.hass, template_var_tups, self._handle_results + ) + self.async_on_remove(result_info.async_remove) + result_info.async_refresh() self.async_write_ha_state() + self._async_update = result_info.async_refresh async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" @@ -272,6 +275,4 @@ class TemplateEntity(Entity): async def async_update(self) -> None: """Call for forced update.""" - for attribute in self._template_attrs: - if attribute.async_update: - attribute.async_update() + self._async_update() diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 980faf4d0a8..5dcee0a7347 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -7,7 +7,11 @@ from homeassistant import exceptions from homeassistant.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.event import async_call_later, async_track_template_result +from homeassistant.helpers.event import ( + TrackTemplate, + async_call_later, + async_track_template_result, +) from homeassistant.helpers.template import result_as_boolean # mypy: allow-untyped-defs, no-check-untyped-defs @@ -34,9 +38,10 @@ async def async_attach_trigger( delay_cancel = None @callback - def template_listener(event, _, last_result, result): + def template_listener(event, updates): """Listen for state changes and calls action.""" nonlocal delay_cancel + result = updates.pop().result if delay_cancel: # pylint: disable=not-callable @@ -94,7 +99,9 @@ async def async_attach_trigger( delay_cancel = async_call_later(hass, period.seconds, call_action) info = async_track_template_result( - hass, value_template, template_listener, automation_info["variables"] + hass, + [TrackTemplate(value_template, automation_info["variables"])], + template_listener, ) unsub = info.async_remove diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 7d1ac9953b6..c38afc139cf 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -71,6 +71,7 @@ from homeassistant.const import ( from homeassistant.core import EVENT_HOMEASSISTANT_START, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import TrackTemplate, async_track_template_result from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.service import async_call_from_config @@ -149,8 +150,10 @@ class UniversalMediaPlayer(MediaPlayerEntity): self.async_schedule_update_ha_state(True) @callback - def _async_on_template_update(event, template, last_result, result): + def _async_on_template_update(event, updates): """Update ha state when dependencies update.""" + result = updates.pop().result + if isinstance(result, TemplateError): self._state_template_result = None else: @@ -158,8 +161,10 @@ class UniversalMediaPlayer(MediaPlayerEntity): self.async_schedule_update_ha_state(True) if self._state_template is not None: - result = self.hass.helpers.event.async_track_template_result( - self._state_template, _async_on_template_update + result = async_track_template_result( + self.hass, + [TrackTemplate(self._state_template, None)], + _async_on_template_update, ) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, callback(lambda _: result.async_refresh()) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 4ed0292a9f4..04ad0ae3d3a 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -15,7 +15,7 @@ from homeassistant.exceptions import ( Unauthorized, ) from homeassistant.helpers import config_validation as cv, entity -from homeassistant.helpers.event import async_track_template_result +from homeassistant.helpers.event import TrackTemplate, async_track_template_result from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -255,19 +255,23 @@ def handle_render_template(hass, connection, msg): variables = msg.get("variables") @callback - def _template_listener(event, template, last_result, result): + def _template_listener(event, updates): + track_template_result = updates.pop() + result = track_template_result.result if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') " "while processing template '%s'", result, - template, + track_template_result.template, ) result = None connection.send_message(messages.event_message(msg["id"], {"result": result})) - info = async_track_template_result(hass, template, _template_listener, variables) + info = async_track_template_result( + hass, [TrackTemplate(template, variables)], _template_listener + ) connection.subscriptions[msg["id"]] = info.async_remove diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 0530ad7cdc7..55bb68fc6ec 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,14 +1,27 @@ """Helpers for listening to events.""" import asyncio +from dataclasses import dataclass from datetime import datetime, timedelta import functools as ft import logging import time -from typing import Any, Awaitable, Callable, Iterable, Optional, Union +from typing import ( + Any, + Awaitable, + Callable, + Dict, + Iterable, + List, + Optional, + Set, + Tuple, + Union, +) import attr from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_NOW, EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, @@ -48,6 +61,37 @@ TRACK_ENTITY_REGISTRY_UPDATED_LISTENER = "track_entity_registry_updated_listener _LOGGER = logging.getLogger(__name__) +@dataclass +class TrackTemplate: + """Class for keeping track of a template with variables. + + The template is template to calculate. + The variables are variables to pass to the template. + """ + + template: Template + variables: TemplateVarsType + + +@dataclass +class TrackTemplateResult: + """Class for result of template tracking. + + template + The template that has changed. + last_result + The output from the template on the last successful run, or None + if no previous successful run. + result + Result from the template run. This will be a string or an + TemplateError if the template resulted in an error. + """ + + template: Template + last_result: Union[str, None, TemplateError] + result: Union[str, TemplateError] + + def threaded_listener_factory(async_factory: Callable[..., Any]) -> CALLBACK_TYPE: """Convert an async event helper to a threaded one.""" @@ -396,13 +440,16 @@ def async_track_template( """ @callback - def state_changed_listener( - event: Event, - template: Template, - last_result: Optional[str], - result: Union[str, TemplateError], + def _template_changed_listener( + event: Event, updates: List[TrackTemplateResult] ) -> None: """Check if condition is correct and run action.""" + track_result = updates.pop() + + template = track_result.template + last_result = track_result.last_result + result = track_result.result + if isinstance(result, TemplateError): _LOGGER.error( "Error while processing template: %s", @@ -411,7 +458,11 @@ def async_track_template( ) return - if result_as_boolean(last_result) or not result_as_boolean(result): + if ( + not isinstance(last_result, TemplateError) + and result_as_boolean(last_result) + or not result_as_boolean(result) + ): return hass.async_run_job( @@ -422,7 +473,7 @@ def async_track_template( ) info = async_track_template_result( - hass, template, state_changed_listener, variables + hass, [TrackTemplate(template, variables)], _template_changed_listener ) return info.async_remove @@ -431,76 +482,89 @@ def async_track_template( track_template = threaded_listener_factory(async_track_template) -_UNCHANGED = object() - - class _TrackTemplateResultInfo: """Handle removal / refresh of tracker.""" def __init__( self, hass: HomeAssistant, - template: Template, + track_templates: Iterable[TrackTemplate], action: Callable, - variables: Optional[TemplateVarsType], ): """Handle removal / refresh of tracker init.""" self.hass = hass - self._template = template - self._template.hass = hass self._action = action - self._variables = variables - self._last_result: Optional[Union[str, TemplateError]] = None + + for track_template_ in track_templates: + track_template_.template.hass = hass + self._track_templates = track_templates + self._all_listener: Optional[Callable] = None self._domains_listener: Optional[Callable] = None self._entities_listener: Optional[Callable] = None - self._info: Optional[RenderInfo] = None - self._last_info: Optional[RenderInfo] = None + + self._last_result: Dict[Template, Union[str, TemplateError]] = {} + self._last_info: Dict[Template, RenderInfo] = {} + self._info: Dict[Template, RenderInfo] = {} + self._last_domains: Set = set() + self._last_entities: Set = set() def async_setup(self) -> None: """Activation of template tracking.""" - self._info = self._template.async_render_to_info(self._variables) - if self._info.exception: - _LOGGER.error( - "Error while processing template: %s", - self._template.template, - exc_info=self._info.exception, - ) + for track_template_ in self._track_templates: + template = track_template_.template + variables = track_template_.variables + + self._info[template] = template.async_render_to_info(variables) + if self._info[template].exception: + _LOGGER.error( + "Error while processing template: %s", + track_template_.template, + exc_info=self._info[template].exception, + ) + + self._last_info = self._info.copy() self._create_listeners() - self._last_info = self._info @property def _needs_all_listener(self) -> bool: - assert self._info + for track_template_ in self._track_templates: + template = track_template_.template - # Tracking all states - if self._info.all_states: - return True + # Tracking all states + if self._info[template].all_states: + return True - # Previous call had an exception - # so we do not know which states - # to track - if self._info.exception: - return True + # Previous call had an exception + # so we do not know which states + # to track + if self._info[template].exception: + return True return False + @property + def _all_templates_are_static(self) -> bool: + for track_template_ in self._track_templates: + if not self._info[track_template_.template].is_static: + return False + + return True + @callback def _create_listeners(self) -> None: - assert self._info - - if self._info.is_static: + if self._all_templates_are_static: return if self._needs_all_listener: self._setup_all_listener() return - if self._info.domains: - self._setup_domains_listener() - - if self._info.entities or self._info.domains: - self._setup_entities_listener() + self._last_entities, self._last_domains = _entities_domains_from_info( + self._info.values() + ) + self._setup_domains_listener(self._last_domains) + self._setup_entities_listener(self._last_domains, self._last_entities) @callback def _cancel_domains_listener(self) -> None: @@ -525,12 +589,11 @@ class _TrackTemplateResultInfo: @callback def _update_listeners(self) -> None: - assert self._info - assert self._last_info - if self._needs_all_listener: if self._all_listener: return + self._last_domains = set() + self._last_entities = set() self._cancel_domains_listener() self._cancel_entities_listener() self._setup_all_listener() @@ -540,27 +603,26 @@ class _TrackTemplateResultInfo: if had_all_listener: self._cancel_all_listener() - domains_changed = self._info.domains != self._last_info.domains + entities, domains = _entities_domains_from_info(self._info.values()) + domains_changed = domains != self._last_domains + if had_all_listener or domains_changed: domains_changed = True self._cancel_domains_listener() - self._setup_domains_listener() + self._setup_domains_listener(domains) - if ( - had_all_listener - or domains_changed - or self._info.entities != self._last_info.entities - ): + if had_all_listener or domains_changed or entities != self._last_entities: self._cancel_entities_listener() - self._setup_entities_listener() + self._setup_entities_listener(domains, entities) + + self._last_domains = domains + self._last_entities = entities @callback - def _setup_entities_listener(self) -> None: - assert self._info - - entities = set(self._info.entities) - for entity_id in self.hass.states.async_entity_ids(self._info.domains): - entities.add(entity_id) + def _setup_entities_listener(self, domains: Set, entities: Set) -> None: + if domains: + entities = entities.copy() + entities.update(self.hass.states.async_entity_ids(domains)) # Entities has changed to none if not entities: @@ -571,15 +633,12 @@ class _TrackTemplateResultInfo: ) @callback - def _setup_domains_listener(self) -> None: - assert self._info - - # Domains has changed to none - if not self._info.domains: + def _setup_domains_listener(self, domains: Set) -> None: + if not domains: return self._domains_listener = async_track_state_added_domain( - self.hass, self._info.domains, self._refresh + self.hass, domains, self._refresh ) @callback @@ -596,40 +655,67 @@ class _TrackTemplateResultInfo: self._cancel_entities_listener() @callback - def async_refresh(self, variables: Any = _UNCHANGED) -> None: + def async_refresh(self) -> None: """Force recalculate the template.""" - if variables is not _UNCHANGED: - self._variables = variables self._refresh(None) @callback def _refresh(self, event: Optional[Event]) -> None: - self._info = self._template.async_render_to_info(self._variables) - self._update_listeners() - self._last_info = self._info + entity_id = event and event.data.get(ATTR_ENTITY_ID) + updates = [] + info_changed = False - try: - result: Union[str, TemplateError] = self._info.result - except TemplateError as ex: - result = ex + for track_template_ in self._track_templates: + template = track_template_.template + if ( + entity_id + and len(self._last_info) > 1 + and not self._last_info[template].filter_lifecycle(entity_id) + ): + continue - # Check to see if the result has changed - if result == self._last_result: + self._info[template] = template.async_render_to_info( + track_template_.variables + ) + info_changed = True + + try: + result: Union[str, TemplateError] = self._info[template].result + except TemplateError as ex: + result = ex + + last_result = self._last_result.get(template) + + # Check to see if the result has changed + if result == last_result: + continue + + if isinstance(result, TemplateError) and isinstance( + last_result, TemplateError + ): + continue + + updates.append(TrackTemplateResult(template, last_result, result)) + + if info_changed: + self._update_listeners() + self._last_info = self._info.copy() + + if not updates: return - if isinstance(result, TemplateError) and isinstance( - self._last_result, TemplateError - ): - return + for track_result in updates: + self._last_result[track_result.template] = track_result.result - self.hass.async_run_job( - self._action, event, self._template, self._last_result, result - ) - self._last_result = result + self.hass.async_run_job(self._action, event, updates) TrackTemplateResultListener = Callable[ - [Event, Template, Optional[str], Union[str, TemplateError]], None + [ + Event, + List[TrackTemplateResult], + ], + None, ] """Type for the listener for template results. @@ -638,14 +724,8 @@ TrackTemplateResultListener = Callable[ event Event that caused the template to change output. None if not triggered by an event. - template - The template that has changed. - last_result - The output from the template on the last successful run, or None - if no previous successful run. - result - Result from the template run. This will be a string or an - TemplateError if the template resulted in an error. + updates + A list of TrackTemplateResult """ @@ -653,9 +733,8 @@ TrackTemplateResultListener = Callable[ @bind_hass def async_track_template_result( hass: HomeAssistant, - template: Template, + track_templates: Iterable[TrackTemplate], action: TrackTemplateResultListener, - variables: Optional[TemplateVarsType] = None, ) -> _TrackTemplateResultInfo: """Add a listener that fires when a the result of a template changes. @@ -675,19 +754,18 @@ def async_track_template_result( ---------- hass Home assistant object. - template - The template to calculate. + track_templates + An iterable of TrackTemplate. + action Callable to call with results. - variables - Variables to pass to the template. Returns ------- Info object used to unregister the listener, and refresh the template. """ - tracker = _TrackTemplateResultInfo(hass, template, action, variables) + tracker = _TrackTemplateResultInfo(hass, track_templates, action) tracker.async_setup() return tracker @@ -1073,3 +1151,16 @@ def process_state_match( parameter_set = set(parameter) return lambda state: state in parameter_set + + +def _entities_domains_from_info(render_infos: Iterable[RenderInfo]) -> Tuple[Set, Set]: + """Combine from multiple RenderInfo.""" + entities = set() + domains = set() + + for render_info in render_infos: + if render_info.entities: + entities.update(render_info.entities) + if render_info.domains: + domains.update(render_info.domains) + return entities, domains diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index b9dc854cd2b..4d559a57c1f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -192,7 +192,7 @@ class RenderInfo: self.entities = frozenset(self.entities) self.domains = frozenset(self.domains) - if self.all_states: + if self.all_states or self.exception: return if not self.domains: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 6fb422e03e7..41d252177a4 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -14,6 +14,8 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.event import ( + TrackTemplate, + TrackTemplateResult, async_call_later, async_track_point_in_time, async_track_point_in_utc_time, @@ -581,22 +583,35 @@ async def test_track_template_result(hass): "{{(states.sensor.test.state|int) + test }}", hass ) - def specific_run_callback(event, template, old_result, new_result): - specific_runs.append(int(new_result)) - - async_track_template_result(hass, template_condition, specific_run_callback) - - @ha.callback - def wildcard_run_callback(event, template, old_result, new_result): - wildcard_runs.append((int(old_result or 0), int(new_result))) - - async_track_template_result(hass, template_condition, wildcard_run_callback) - - async def wildercard_run_callback(event, template, old_result, new_result): - wildercard_runs.append((int(old_result or 0), int(new_result))) + def specific_run_callback(event, updates): + track_result = updates.pop() + specific_runs.append(int(track_result.result)) async_track_template_result( - hass, template_condition_var, wildercard_run_callback, {"test": 5} + hass, [TrackTemplate(template_condition, None)], specific_run_callback + ) + + @ha.callback + def wildcard_run_callback(event, updates): + track_result = updates.pop() + wildcard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + + async_track_template_result( + hass, [TrackTemplate(template_condition, None)], wildcard_run_callback + ) + + async def wildercard_run_callback(event, updates): + track_result = updates.pop() + wildercard_runs.append( + (int(track_result.last_result or 0), int(track_result.result)) + ) + + async_track_template_result( + hass, + [TrackTemplate(template_condition_var, {"test": 5})], + wildercard_run_callback, ) await hass.async_block_till_done() @@ -661,13 +676,15 @@ async def test_track_template_result_complex(hass): """ template_complex = Template(template_complex_str, hass) - def specific_run_callback(event, template, old_result, new_result): - specific_runs.append(new_result) + def specific_run_callback(event, updates): + specific_runs.append(updates.pop().result) hass.states.async_set("light.one", "on") hass.states.async_set("lock.one", "locked") - async_track_template_result(hass, template_complex, specific_run_callback) + async_track_template_result( + hass, [TrackTemplate(template_complex, None)], specific_run_callback + ) await hass.async_block_till_done() hass.states.async_set("sensor.domain", "light") @@ -742,14 +759,16 @@ async def test_track_template_result_with_wildcard(hass): """ template_complex = Template(template_complex_str, hass) - def specific_run_callback(event, template, old_result, new_result): - specific_runs.append(new_result) + def specific_run_callback(event, updates): + specific_runs.append(updates.pop().result) hass.states.async_set("cover.office_drapes", "closed") hass.states.async_set("cover.office_window", "closed") hass.states.async_set("cover.office_skylight", "open") - async_track_template_result(hass, template_complex, specific_run_callback) + async_track_template_result( + hass, [TrackTemplate(template_complex, None)], specific_run_callback + ) await hass.async_block_till_done() hass.states.async_set("cover.office_window", "open") @@ -786,10 +805,12 @@ async def test_track_template_result_with_group(hass): """ template_complex = Template(template_complex_str, hass) - def specific_run_callback(event, template, old_result, new_result): - specific_runs.append(new_result) + def specific_run_callback(event, updates): + specific_runs.append(updates.pop().result) - async_track_template_result(hass, template_complex, specific_run_callback) + async_track_template_result( + hass, [TrackTemplate(template_complex, None)], specific_run_callback + ) await hass.async_block_till_done() hass.states.async_set("sensor.power_1", 100.1) @@ -827,13 +848,12 @@ async def test_track_template_result_and_conditional(hass): template = Template(template_str, hass) - def specific_run_callback(event, template, old_result, new_result): - import pprint + def specific_run_callback(event, updates): + specific_runs.append(updates.pop().result) - pprint.pprint([event, template, old_result, new_result]) - specific_runs.append(new_result) - - async_track_template_result(hass, template, specific_run_callback) + async_track_template_result( + hass, [TrackTemplate(template, None)], specific_run_callback + ) await hass.async_block_till_done() hass.states.async_set("light.b", "on") @@ -869,21 +889,26 @@ async def test_track_template_result_iterator(hass): iterator_runs = [] @ha.callback - def iterator_callback(event, template, old_result, new_result): - iterator_runs.append(new_result) + def iterator_callback(event, updates): + iterator_runs.append(updates.pop().result) async_track_template_result( hass, - Template( - """ + [ + TrackTemplate( + Template( + """ {% for state in states.sensor %} {% if state.state == 'on' %} {{ state.entity_id }}, {% endif %} {% endfor %} """, - hass, - ), + hass, + ), + None, + ) + ], iterator_callback, ) await hass.async_block_till_done() @@ -896,16 +921,21 @@ async def test_track_template_result_iterator(hass): filter_runs = [] @ha.callback - def filter_callback(event, template, old_result, new_result): - filter_runs.append(new_result) + def filter_callback(event, updates): + filter_runs.append(updates.pop().result) async_track_template_result( hass, - Template( - """{{ states.sensor|selectattr("state","equalto","on") + [ + TrackTemplate( + Template( + """{{ states.sensor|selectattr("state","equalto","on") |join(",", attribute="entity_id") }}""", - hass, - ), + hass, + ), + None, + ) + ], filter_callback, ) await hass.async_block_till_done() @@ -931,21 +961,42 @@ async def test_track_template_result_errors(hass, caplog): syntax_error_runs = [] not_exist_runs = [] - def syntax_error_listener(event, template, last_result, result): - syntax_error_runs.append((event, template, last_result, result)) + @ha.callback + def syntax_error_listener(event, updates): + track_result = updates.pop() + syntax_error_runs.append( + ( + event, + track_result.template, + track_result.last_result, + track_result.result, + ) + ) - async_track_template_result(hass, template_syntax_error, syntax_error_listener) + async_track_template_result( + hass, [TrackTemplate(template_syntax_error, None)], syntax_error_listener + ) await hass.async_block_till_done() assert len(syntax_error_runs) == 0 assert "TemplateSyntaxError" in caplog.text + @ha.callback + def not_exist_runs_error_listener(event, updates): + template_track = updates.pop() + not_exist_runs.append( + ( + event, + template_track.template, + template_track.last_result, + template_track.result, + ) + ) + async_track_template_result( hass, - template_not_exist, - lambda event, template, last_result, result: ( - not_exist_runs.append((event, template, last_result, result)) - ), + [TrackTemplate(template_not_exist, None)], + not_exist_runs_error_listener, ) await hass.async_block_till_done() @@ -990,10 +1041,13 @@ async def test_track_template_result_refresh_cancel(hass): refresh_runs = [] - def refresh_listener(event, template, last_result, result): - refresh_runs.append(result) + @ha.callback + def refresh_listener(event, updates): + refresh_runs.append(updates.pop().result) - info = async_track_template_result(hass, template_refresh, refresh_listener) + info = async_track_template_result( + hass, [TrackTemplate(template_refresh, None)], refresh_listener + ) await hass.async_block_till_done() hass.states.async_set("switch.test", "off") @@ -1020,7 +1074,9 @@ async def test_track_template_result_refresh_cancel(hass): refresh_runs = [] info = async_track_template_result( - hass, template_refresh, refresh_listener, {"value": "duck"} + hass, + [TrackTemplate(template_refresh, {"value": "duck"})], + refresh_listener, ) await hass.async_block_till_done() info.async_refresh() @@ -1032,9 +1088,132 @@ async def test_track_template_result_refresh_cancel(hass): await hass.async_block_till_done() assert refresh_runs == ["duck"] - info.async_refresh({"value": "dog"}) + +async def test_async_track_template_result_multiple_templates(hass): + """Test tracking multiple templates.""" + + template_1 = Template("{{ states.switch.test.state == 'on' }}") + template_2 = Template("{{ states.switch.test.state == 'on' }}") + template_3 = Template("{{ states.switch.test.state == 'off' }}") + template_4 = Template( + "{{ states.binary_sensor | map(attribute='entity_id') | list }}" + ) + + refresh_runs = [] + + @ha.callback + def refresh_listener(event, updates): + refresh_runs.append(updates) + + async_track_template_result( + hass, + [ + TrackTemplate(template_1, None), + TrackTemplate(template_2, None), + TrackTemplate(template_3, None), + TrackTemplate(template_4, None), + ], + refresh_listener, + ) + + hass.states.async_set("switch.test", "on") await hass.async_block_till_done() - assert refresh_runs == ["duck", "dog"] + + assert refresh_runs == [ + [ + TrackTemplateResult(template_1, None, "True"), + TrackTemplateResult(template_2, None, "True"), + TrackTemplateResult(template_3, None, "False"), + ] + ] + + refresh_runs = [] + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() + + assert refresh_runs == [ + [ + TrackTemplateResult(template_1, "True", "False"), + TrackTemplateResult(template_2, "True", "False"), + TrackTemplateResult(template_3, "False", "True"), + ] + ] + + refresh_runs = [] + hass.states.async_set("binary_sensor.test", "off") + await hass.async_block_till_done() + + assert refresh_runs == [ + [TrackTemplateResult(template_4, None, "['binary_sensor.test']")] + ] + + +async def test_async_track_template_result_multiple_templates_mixing_domain(hass): + """Test tracking multiple templates when tracking entities and an entire domain.""" + + template_1 = Template("{{ states.switch.test.state == 'on' }}") + template_2 = Template("{{ states.switch.test.state == 'on' }}") + template_3 = Template("{{ states.switch.test.state == 'off' }}") + template_4 = Template("{{ states.switch | map(attribute='entity_id') | list }}") + + refresh_runs = [] + + @ha.callback + def refresh_listener(event, updates): + refresh_runs.append(updates) + + async_track_template_result( + hass, + [ + TrackTemplate(template_1, None), + TrackTemplate(template_2, None), + TrackTemplate(template_3, None), + TrackTemplate(template_4, None), + ], + refresh_listener, + ) + + hass.states.async_set("switch.test", "on") + await hass.async_block_till_done() + + assert refresh_runs == [ + [ + TrackTemplateResult(template_1, None, "True"), + TrackTemplateResult(template_2, None, "True"), + TrackTemplateResult(template_3, None, "False"), + TrackTemplateResult(template_4, None, "['switch.test']"), + ] + ] + + refresh_runs = [] + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() + + assert refresh_runs == [ + [ + TrackTemplateResult(template_1, "True", "False"), + TrackTemplateResult(template_2, "True", "False"), + TrackTemplateResult(template_3, "False", "True"), + ] + ] + + refresh_runs = [] + hass.states.async_set("binary_sensor.test", "off") + await hass.async_block_till_done() + + assert refresh_runs == [] + + refresh_runs = [] + hass.states.async_set("switch.new", "off") + await hass.async_block_till_done() + + assert refresh_runs == [ + [ + TrackTemplateResult( + template_4, "['switch.test']", "['switch.new', 'switch.test']" + ) + ] + ] async def test_track_same_state_simple_no_trigger(hass): From 99d830551a17acbaed60194f591c84321b8e5dba Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Tue, 1 Sep 2020 13:06:30 +0800 Subject: [PATCH 539/862] Bump yeelight to 0.5.3 (#39542) --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 92baced836a..1e8c0472fdd 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -3,7 +3,7 @@ "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", "requirements": [ - "yeelight==0.5.2" + "yeelight==0.5.3" ], "codeowners": [ "@rytilahti", diff --git a/requirements_all.txt b/requirements_all.txt index c7d090f9514..1f2681ec9ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2273,7 +2273,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.1.6 # homeassistant.components.yeelight -yeelight==0.5.2 +yeelight==0.5.3 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f864a2458ab..6484a7dab4b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1046,7 +1046,7 @@ wolf_smartset==0.1.4 xmltodict==0.12.0 # homeassistant.components.yeelight -yeelight==0.5.2 +yeelight==0.5.3 # homeassistant.components.zeroconf zeroconf==0.28.3 From 762d7357b51c5f3b17524e537e1e731d4a38a5bb Mon Sep 17 00:00:00 2001 From: jdelaney72 <20731268+jdelaney72@users.noreply.github.com> Date: Tue, 1 Sep 2020 03:42:39 -0700 Subject: [PATCH 540/862] Fix outdated api url in noaa_tides (#39370) * Fix outdated dependency in noaa_tides * Catch exceptions when instantiating new Station * Add myself to codeowners --- CODEOWNERS | 1 + .../components/noaa_tides/manifest.json | 4 +-- homeassistant/components/noaa_tides/sensor.py | 31 +++++++++++++------ requirements_all.txt | 6 ++-- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 85cde0973f9..0bb10972910 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -281,6 +281,7 @@ homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff +homeassistant/components/noaa_tides/* @jdelaney72 homeassistant/components/notify/* @home-assistant/core homeassistant/components/notify_events/* @matrozov @papajojo homeassistant/components/notion/* @bachya diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index 3e95ff523b7..f0343d88c84 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -2,6 +2,6 @@ "domain": "noaa_tides", "name": "NOAA Tides", "documentation": "https://www.home-assistant.io/integrations/noaa_tides", - "requirements": ["py_noaa==0.3.0"], - "codeowners": [] + "requirements": ["noaa-coops==0.1.8"], + "codeowners": ["@jdelaney72"] } diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 063a163a8ab..a0453e3acb1 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -2,7 +2,8 @@ from datetime import datetime, timedelta import logging -from py_noaa import coops # pylint: disable=import-error +import noaa_coops as coops # pylint: disable=import-error +import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -12,6 +13,7 @@ from homeassistant.const import ( CONF_TIME_ZONE, CONF_UNIT_SYSTEM, ) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -51,24 +53,35 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: unit_system = UNIT_SYSTEMS[0] - noaa_sensor = NOAATidesAndCurrentsSensor(name, station_id, timezone, unit_system) - - noaa_sensor.update() - if noaa_sensor.data is None: - _LOGGER.error("Unable to setup NOAA Tides Sensor") + try: + station = coops.Station(station_id, unit_system) + except KeyError: + _LOGGER.error("NOAA Tides Sensor station_id %s does not exist", station_id) return + except requests.exceptions.ConnectionError as exception: + _LOGGER.error( + "Connection error during setup in NOAA Tides Sensor for station_id: %s", + station_id, + ) + raise PlatformNotReady from exception + + noaa_sensor = NOAATidesAndCurrentsSensor( + name, station_id, timezone, unit_system, station + ) + add_entities([noaa_sensor], True) class NOAATidesAndCurrentsSensor(Entity): """Representation of a NOAA Tides and Currents sensor.""" - def __init__(self, name, station_id, timezone, unit_system): + def __init__(self, name, station_id, timezone, unit_system, station): """Initialize the sensor.""" self._name = name self._station_id = station_id self._timezone = timezone self._unit_system = unit_system + self._station = station self.data = None @property @@ -110,15 +123,13 @@ class NOAATidesAndCurrentsSensor(Entity): def update(self): """Get the latest data from NOAA Tides and Currents API.""" - begin = datetime.now() delta = timedelta(days=2) end = begin + delta try: - df_predictions = coops.get_data( + df_predictions = self._station.get_data( begin_date=begin.strftime("%Y%m%d %H:%M"), end_date=end.strftime("%Y%m%d %H:%M"), - stationid=self._station_id, product="predictions", datum="MLLW", interval="hilo", diff --git a/requirements_all.txt b/requirements_all.txt index 1f2681ec9ea..90d54dc36a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -966,6 +966,9 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 +# homeassistant.components.noaa_tides +noaa-coops==0.1.8 + # homeassistant.components.notify_events notify-events==1.0.4 @@ -1203,9 +1206,6 @@ pyW800rf32==0.1 # homeassistant.components.nextbus py_nextbusnext==0.1.4 -# homeassistant.components.noaa_tides -# py_noaa==0.3.0 - # homeassistant.components.ads pyads==3.2.2 From 69b3da48b1da5e61ba7fae78d56a7536a051f995 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Tue, 1 Sep 2020 13:01:47 +0200 Subject: [PATCH 541/862] Adds missing name property to KNX weather device (#39547) --- homeassistant/components/knx/weather.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index aed376ec066..97500ef8194 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -23,6 +23,11 @@ class KNXWeather(WeatherEntity): """Initialize of a KNX sensor.""" self.device = device + @property + def name(self): + """Return the name of the weather device.""" + return self.device.name + @property def temperature(self): """Return current temperature.""" From b541abc55197075e7cd53e93e29dcaab4345d5d9 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 1 Sep 2020 12:03:41 +0100 Subject: [PATCH 542/862] =?UTF-8?q?Bump=20openwrt-luci-rpc=20version:=201.?= =?UTF-8?q?1.3=20=E2=86=92=201.1.5=20(#39545)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/luci/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index cd4c278c2fb..5699972a053 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -2,6 +2,6 @@ "domain": "luci", "name": "OpenWRT (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", - "requirements": ["openwrt-luci-rpc==1.1.3"], + "requirements": ["openwrt-luci-rpc==1.1.5"], "codeowners": ["@fbradyirl", "@mzdrale"] } diff --git a/requirements_all.txt b/requirements_all.txt index 90d54dc36a5..b8a1bcc565c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ opensensemap-api==0.1.5 openwebifpy==3.1.1 # homeassistant.components.luci -openwrt-luci-rpc==1.1.3 +openwrt-luci-rpc==1.1.5 # homeassistant.components.oru oru==0.1.11 From dde0dab3db41a0d110165ff66c8d839b595b9a2b Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 1 Sep 2020 15:04:42 +0300 Subject: [PATCH 543/862] Handle missing values in Shelly sensors (#39515) --- homeassistant/components/shelly/sensor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 512c387c561..815b83d1882 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -88,6 +88,10 @@ class ShellySensor(ShellyBlockEntity, Entity): @property def state(self): """Value of sensor.""" + value = getattr(self.block, self.attribute) + if value is None: + return None + if self.attribute in [ "deviceTemp", "extTemp", @@ -95,13 +99,13 @@ class ShellySensor(ShellyBlockEntity, Entity): "overpowerValue", "power", ]: - return round(getattr(self.block, self.attribute), 1) + return round(value, 1) # Energy unit change from Wmin or Wh to kWh if self.info[aioshelly.BLOCK_VALUE_UNIT] == "Wmin": - return round(getattr(self.block, self.attribute) / 60 / 1000, 2) + return round(value / 60 / 1000, 2) if self.info[aioshelly.BLOCK_VALUE_UNIT] == "Wh": - return round(getattr(self.block, self.attribute) / 1000, 2) - return getattr(self.block, self.attribute) + return round(value / 1000, 2) + return value @property def unit_of_measurement(self): From 32094c8773511f2ca1f8eef53c7e01e8f83266b2 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Tue, 1 Sep 2020 14:05:09 +0200 Subject: [PATCH 544/862] Fix missing end tag in translation key (#39546) --- homeassistant/components/insteon/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index d6690bd4860..b0130910c5f 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -40,7 +40,7 @@ }, "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, "options": { @@ -106,4 +106,4 @@ "input_error": "Invalid entries, please check your values." } } -} \ No newline at end of file +} From 963651d6f23fd4903e862535956f64b1f90ac0d2 Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 1 Sep 2020 15:08:37 +0300 Subject: [PATCH 545/862] Add support for authenticated Shelly devices (#39461) * Add support for authenticated Shelly devices * Fix comment typos * Update homeassistant/components/shelly/config_flow.py Co-authored-by: Maciej Bieniek * Fix unauthenticated devices * Update homeassistant/components/shelly/config_flow.py Co-authored-by: Martin Hjelmare * Code review fixes * More code review fixes * Fix typo * Update homeassistant/components/shelly/config_flow.py Co-authored-by: Martin Hjelmare Co-authored-by: Maciej Bieniek Co-authored-by: Martin Hjelmare --- homeassistant/components/shelly/__init__.py | 12 +- .../components/shelly/config_flow.py | 71 +++++++-- homeassistant/components/shelly/strings.json | 10 +- .../components/shelly/translations/en.json | 16 +- tests/components/shelly/test_config_flow.py | 142 +++++++++++++++++- 5 files changed, 220 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 427bd0148f2..ebc3d287e5e 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -8,7 +8,12 @@ import aioshelly import async_timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( @@ -35,7 +40,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: async with async_timeout.timeout(5): device = await aioshelly.Device.create( - entry.data["host"], aiohttp_client.async_get_clientsession(hass) + entry.data[CONF_HOST], + aiohttp_client.async_get_clientsession(hass), + entry.data.get(CONF_USERNAME), + entry.data.get(CONF_PASSWORD), ) except (asyncio.TimeoutError, OSError) as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index c464f0d7adb..cd35f7d2552 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -8,25 +8,29 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({"host": str}) +HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) HTTP_CONNECT_ERRORS = (asyncio.TimeoutError, aiohttp.ClientError) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: core.HomeAssistant, host, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ async with async_timeout.timeout(5): device = await aioshelly.Device.create( - data["host"], aiohttp_client.async_get_clientsession(hass) + host, + aiohttp_client.async_get_clientsession(hass), + data.get(CONF_USERNAME), + data.get(CONF_PASSWORD), ) await device.shutdown() @@ -47,34 +51,71 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: + host = user_input[CONF_HOST] try: - info = await self._async_get_info(user_input["host"]) + info = await self._async_get_info(host) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" - else: + await self.async_set_unique_id(info["mac"]) + self._abort_if_unique_id_configured({CONF_HOST: host}) + self.host = host if info["auth"]: - return self.async_abort(reason="auth_not_supported") + return await self.async_step_credentials() try: - device_info = await validate_input(self.hass, user_input) + device_info = await validate_input(self.hass, self.host, {}) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - await self.async_set_unique_id(device_info["mac"]) return self.async_create_entry( - title=device_info["title"] or user_input["host"], + title=device_info["title"] or self.host, data=user_input, ) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", data_schema=HOST_SCHEMA, errors=errors + ) + + async def async_step_credentials(self, user_input=None): + """Handle the credentials step.""" + errors = {} + if user_input is not None: + try: + device_info = await validate_input(self.hass, self.host, user_input) + except aiohttp.ClientResponseError as error: + if error.status == 401: + errors["base"] = "invalid_auth" + else: + errors["base"] = "cannot_connect" + except HTTP_CONNECT_ERRORS: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=device_info["title"] or self.host, + data={**user_input, CONF_HOST: self.host}, + ) + else: + user_input = {} + + schema = vol.Schema( + { + vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME)): str, + vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD)): str, + } + ) + + return self.async_show_form( + step_id="credentials", data_schema=schema, errors=errors ) async def async_step_zeroconf(self, zeroconf_info): @@ -87,11 +128,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") - if info["auth"]: - return self.async_abort(reason="auth_not_supported") - await self.async_set_unique_id(info["mac"]) - self._abort_if_unique_id_configured({"host": zeroconf_info["host"]}) + self._abort_if_unique_id_configured({CONF_HOST: zeroconf_info["host"]}) self.host = zeroconf_info["host"] # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {"name": zeroconf_info["host"]} @@ -101,8 +139,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle discovery confirm.""" errors = {} if user_input is not None: + if self.info["auth"]: + return await self.async_step_credentials() + try: - device_info = await validate_input(self.hass, {"host": self.host}) + device_info = await validate_input(self.hass, self.host, {}) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 9c5f5707914..16dc331e452 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -8,14 +8,20 @@ "host": "[%key:common::config_flow::data::host%]" } }, + "credentials": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, "confirm_discovery": { "description": "Do you want to set up the {model} at {host}?" } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]", - "auth_not_supported": "Shelly devices requiring authentication are not currently supported." + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index ebcb1976516..5c3e86e73ad 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -1,21 +1,27 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { - "auth_not_supported": "Shelly devices requiring authentication are not currently supported.", - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "flow_title": "Shelly: {name}", "step": { "confirm_discovery": { "description": "Do you want to set up the {model} at {host}?" }, + "credentials": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + }, "user": { "data": { - "host": "Host" + "host": "[%key:common::config_flow::data::host%]" } } } diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 9acd52b6e8e..93192e89df3 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Shelly config flow.""" import asyncio +import aiohttp import pytest from homeassistant import config_entries, setup @@ -50,7 +51,7 @@ async def test_form(hass): async def test_form_auth(hass): - """Test we can't manually configure if auth is required.""" + """Test manual configuration if auth is required.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -66,8 +67,36 @@ async def test_form_auth(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == "abort" - assert result2["reason"] == "auth_not_supported" + assert result2["type"] == "form" + assert result["errors"] == {} + + with patch( + "aioshelly.Device.create", + return_value=Mock( + shutdown=AsyncMock(), + settings={"name": "Test name", "device": {"mac": "test-mac"}}, + ), + ), patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.shelly.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"username": "test username", "password": "test password"}, + ) + + assert result3["type"] == "create_entry" + assert result3["title"] == "Test name" + assert result3["data"] == { + "host": "1.1.1.1", + "username": "test username", + "password": "test password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( @@ -103,7 +132,9 @@ async def test_form_errors_test_connection(hass, error): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aioshelly.get_info", return_value={"auth": False}), patch( + with patch( + "aioshelly.get_info", return_value={"mac": "test-mac", "auth": False} + ), patch( "aioshelly.Device.create", side_effect=exc, ): @@ -116,6 +147,68 @@ async def test_form_errors_test_connection(hass, error): assert result2["errors"] == {"base": base_error} +async def test_form_already_configured(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0"} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "aioshelly.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + +@pytest.mark.parametrize( + "error", + [ + (aiohttp.ClientResponseError(Mock(), (), status=400), "cannot_connect"), + (aiohttp.ClientResponseError(Mock(), (), status=401), "invalid_auth"), + (asyncio.TimeoutError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_auth_errors_test_connection(hass, error): + """Test we handle errors in authenticated devices.""" + exc, base_error = error + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aioshelly.get_info", return_value={"mac": "test-mac", "auth": True}): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + with patch( + "aioshelly.Device.create", + side_effect=exc, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"username": "test username", "password": "test password"}, + ) + assert result3["type"] == "form" + assert result3["errors"] == {"base": base_error} + + async def test_zeroconf(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -232,7 +325,7 @@ async def test_zeroconf_cannot_connect(hass): async def test_zeroconf_require_auth(hass): - """Test we get the form.""" + """Test zeroconf if auth is required.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( @@ -244,8 +337,43 @@ async def test_zeroconf_require_auth(hass): data={"host": "1.1.1.1", "name": "shelly1pm-12345"}, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" - assert result["reason"] == "auth_not_supported" + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == "form" + assert result2["errors"] == {} + + with patch( + "aioshelly.Device.create", + return_value=Mock( + shutdown=AsyncMock(), + settings={"name": "Test name", "device": {"mac": "test-mac"}}, + ), + ), patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.shelly.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"username": "test username", "password": "test password"}, + ) + + assert result3["type"] == "create_entry" + assert result3["title"] == "Test name" + assert result3["data"] == { + "host": "1.1.1.1", + "username": "test username", + "password": "test 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_zeroconf_not_shelly(hass): From aa476b392cf5b0022fbc2508f749cf53aac2d3e9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Sep 2020 14:18:11 +0200 Subject: [PATCH 546/862] Filter out disconnected Shelly sensors (#39516) --- homeassistant/components/shelly/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 815b83d1882..953499762a0 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -37,7 +37,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for block in wrapper.device.blocks: for attr in SENSORS: - if not hasattr(block, attr): + # Filter out non-existing sensors and sensors without a value + if getattr(block, attr, None) is None: continue sensors.append(ShellySensor(wrapper, block, attr)) From 151c0d9761941655caf8b7047158449eabe9cea9 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Tue, 1 Sep 2020 13:24:23 +0100 Subject: [PATCH 547/862] Provide user-defined actions to app (#38572) * Start moving stuff to iOS * Load config on to hass.data * Remove un used logging * Switch to Rest API * Add schema * Return whole config in new view and leave old 100 % the same * Update doc strings * MartinHjelmare feedback * Move register view to async_setup_entry --- homeassistant/components/ios/__init__.py | 88 ++++++++++++++++++------ homeassistant/components/ios/const.py | 10 +++ 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 9eec508bbd5..cb8603d5669 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -12,9 +12,20 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, discovery from homeassistant.util.json import load_json, save_json -_LOGGER = logging.getLogger(__name__) +from .const import ( + CONF_ACTION_BACKGROUND_COLOR, + CONF_ACTION_ICON, + CONF_ACTION_ICON_COLOR, + CONF_ACTION_ICON_ICON, + CONF_ACTION_LABEL, + CONF_ACTION_LABEL_COLOR, + CONF_ACTION_LABEL_TEXT, + CONF_ACTION_NAME, + CONF_ACTIONS, + DOMAIN, +) -DOMAIN = "ios" +_LOGGER = logging.getLogger(__name__) CONF_PUSH = "push" CONF_PUSH_CATEGORIES = "categories" @@ -32,6 +43,8 @@ CONF_PUSH_ACTIONS_CONTEXT = "context" CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE = "textInputButtonTitle" CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER = "textInputPlaceholder" +CONF_USER = "user" + ATTR_FOREGROUND = "foreground" ATTR_BACKGROUND = "background" @@ -87,7 +100,7 @@ BATTERY_STATES = [ ATTR_DEVICES = "devices" -ACTION_SCHEMA = vol.Schema( +PUSH_ACTION_SCHEMA = vol.Schema( { vol.Required(CONF_PUSH_ACTIONS_IDENTIFIER): vol.Upper, vol.Required(CONF_PUSH_ACTIONS_TITLE): cv.string, @@ -107,25 +120,40 @@ ACTION_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -ACTION_SCHEMA_LIST = vol.All(cv.ensure_list, [ACTION_SCHEMA]) +PUSH_ACTION_LIST_SCHEMA = vol.All(cv.ensure_list, [PUSH_ACTION_SCHEMA]) + +PUSH_CATEGORY_SCHEMA = vol.Schema( + { + vol.Required(CONF_PUSH_CATEGORIES_NAME): cv.string, + vol.Required(CONF_PUSH_CATEGORIES_IDENTIFIER): vol.Lower, + vol.Required(CONF_PUSH_CATEGORIES_ACTIONS): PUSH_ACTION_LIST_SCHEMA, + } +) + +PUSH_CATEGORY_LIST_SCHEMA = vol.All(cv.ensure_list, [PUSH_CATEGORY_SCHEMA]) + +ACTION_SCHEMA = vol.Schema( + { + vol.Required(CONF_ACTION_NAME): cv.string, + vol.Optional(CONF_ACTION_BACKGROUND_COLOR): cv.string, + vol.Optional(CONF_ACTION_LABEL): { + vol.Optional(CONF_ACTION_LABEL_TEXT): cv.string, + vol.Optional(CONF_ACTION_LABEL_COLOR): cv.string, + }, + vol.Optional(CONF_ACTION_ICON): { + vol.Optional(CONF_ACTION_ICON_ICON): cv.string, + vol.Optional(CONF_ACTION_ICON_COLOR): cv.string, + }, + }, +) + +ACTION_LIST_SCHEMA = vol.All(cv.ensure_list, [ACTION_SCHEMA]) CONFIG_SCHEMA = vol.Schema( { DOMAIN: { - CONF_PUSH: { - CONF_PUSH_CATEGORIES: vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_PUSH_CATEGORIES_NAME): cv.string, - vol.Required(CONF_PUSH_CATEGORIES_IDENTIFIER): vol.Lower, - vol.Required( - CONF_PUSH_CATEGORIES_ACTIONS - ): ACTION_SCHEMA_LIST, - } - ], - ) - } + CONF_PUSH: {CONF_PUSH_CATEGORIES: PUSH_CATEGORY_LIST_SCHEMA}, + CONF_ACTIONS: ACTION_LIST_SCHEMA, } }, extra=vol.ALLOW_EXTRA, @@ -226,7 +254,10 @@ async def async_setup(hass, config): if ios_config == {}: ios_config[ATTR_DEVICES] = {} - ios_config[CONF_PUSH] = (conf or {}).get(CONF_PUSH, {}) + ios_config[CONF_USER] = conf or {} + + if CONF_PUSH not in ios_config[CONF_USER]: + ios_config[CONF_USER][CONF_PUSH] = {} hass.data[DOMAIN] = ios_config @@ -250,7 +281,8 @@ async def async_setup_entry(hass, entry): ) hass.http.register_view(iOSIdentifyDeviceView(hass.config.path(CONFIGURATION_FILE))) - hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_PUSH])) + hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_USER][CONF_PUSH])) + hass.http.register_view(iOSConfigView(hass.data[DOMAIN][CONF_USER])) return True @@ -272,6 +304,22 @@ class iOSPushConfigView(HomeAssistantView): return self.json(self.push_config) +class iOSConfigView(HomeAssistantView): + """A view that provides the whole user-defined configuration.""" + + url = "/api/ios/config" + name = "api:ios:config" + + def __init__(self, config): + """Init the view.""" + self.config = config + + @callback + def get(self, request): + """Handle the GET request for the user-defined configuration.""" + return self.json(self.config) + + class iOSIdentifyDeviceView(HomeAssistantView): """A view that accepts device identification requests.""" diff --git a/homeassistant/components/ios/const.py b/homeassistant/components/ios/const.py index 5fc921b7a44..3e6b2155add 100644 --- a/homeassistant/components/ios/const.py +++ b/homeassistant/components/ios/const.py @@ -1,3 +1,13 @@ """Const for iOS.""" DOMAIN = "ios" + +CONF_ACTION_NAME = "name" +CONF_ACTION_BACKGROUND_COLOR = "background_color" +CONF_ACTION_LABEL = "label" +CONF_ACTION_LABEL_COLOR = "color" +CONF_ACTION_LABEL_TEXT = "text" +CONF_ACTION_ICON = "icon" +CONF_ACTION_ICON_COLOR = "color" +CONF_ACTION_ICON_ICON = "icon" +CONF_ACTIONS = "actions" From 352995c663d3c2632db09b187ea12cbb77f52872 Mon Sep 17 00:00:00 2001 From: rajlaud <50647620+rajlaud@users.noreply.github.com> Date: Tue, 1 Sep 2020 07:28:34 -0500 Subject: [PATCH 548/862] Squeezebox scene fixes (#38214) * Support for playlist in media_content_id * Support include playlist index in content_id * Add media_player.media_stop support to squeezebox --- .../components/squeezebox/manifest.json | 2 +- .../components/squeezebox/media_player.py | 24 ++++++++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index b682887779b..2ebde216130 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -6,7 +6,7 @@ "@rajlaud" ], "requirements": [ - "pysqueezebox==0.2.4" + "pysqueezebox==0.3.1" ], "config_flow": true } diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 2dbad960227..a4b62d33f39 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -1,5 +1,6 @@ """Support for interfacing to the Logitech SqueezeBox API.""" import asyncio +import json import logging from pysqueezebox import Server, async_discover @@ -10,6 +11,7 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEn from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -18,6 +20,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, @@ -80,6 +83,7 @@ SUPPORT_SQUEEZEBOX = ( | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_STOP ) PLATFORM_SCHEMA = vol.All( @@ -334,11 +338,20 @@ class SqueezeBoxEntity(MediaPlayerEntity): @property def media_content_id(self): """Content ID of current playing media.""" + if not self._player.playlist: + return None + if len(self._player.playlist) > 1: + urls = [{"url": track["url"]} for track in self._player.playlist] + return json.dumps({"index": self._player.current_index, "urls": urls}) return self._player.url @property def media_content_type(self): """Content type of current playing media.""" + if not self._player.playlist: + return None + if len(self._player.playlist) > 1: + return MEDIA_TYPE_PLAYLIST return MEDIA_TYPE_MUSIC @property @@ -424,6 +437,10 @@ class SqueezeBoxEntity(MediaPlayerEntity): """Mute (true) or unmute (false) media player.""" await self._player.async_set_muting(mute) + async def async_media_stop(self): + """Send stop command to media player.""" + await self._player.async_stop() + async def async_media_play_pause(self): """Send pause command to media player.""" await self._player.async_toggle_pause() @@ -462,7 +479,12 @@ class SqueezeBoxEntity(MediaPlayerEntity): if kwargs.get(ATTR_MEDIA_ENQUEUE): cmd = "add" - await self._player.async_load_url(media_id, cmd) + if media_type == MEDIA_TYPE_PLAYLIST: + content = json.loads(media_id) + await self._player.async_load_playlist(content["urls"], cmd) + await self._player.async_index(content["index"]) + else: + await self._player.async_load_url(media_id, cmd) async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" diff --git a/requirements_all.txt b/requirements_all.txt index b8a1bcc565c..e1dfedf8b92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1652,7 +1652,7 @@ pysonos==0.0.32 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.2.4 +pysqueezebox==0.3.1 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6484a7dab4b..9f9db1ad90a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -797,7 +797,7 @@ pysonos==0.0.32 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.2.4 +pysqueezebox==0.3.1 # homeassistant.components.syncthru pysyncthru==0.7.0 From 762537d82d1255746275bdd18e426abf5bcc09a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Sep 2020 08:53:50 -0500 Subject: [PATCH 549/862] Deprecate manually passing entity ids to template entities (#39382) It is no longer necessary to provide a list of entities to monitor to the template platforms. The template is now re-evaluated whenever a referenced entity changes state, and new entities are automaticlly discovered. Automatic analysis can now determine the entities for all templates without the need for manual setup. --- .../components/template/binary_sensor.py | 33 ++++++++------ homeassistant/components/template/cover.py | 1 + homeassistant/components/template/fan.py | 41 +++++++++-------- homeassistant/components/template/light.py | 43 +++++++++--------- homeassistant/components/template/sensor.py | 35 ++++++++------- homeassistant/components/template/switch.py | 27 ++++++----- homeassistant/components/template/vacuum.py | 45 ++++++++++--------- 7 files changed, 123 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index a15226e7e64..bd6cb55b880 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -36,20 +36,25 @@ CONF_DELAY_ON = "delay_on" CONF_DELAY_OFF = "delay_off" CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" -SENSOR_SCHEMA = vol.Schema( - { - vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_ICON_TEMPLATE): cv.template, - vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, - vol.Optional(CONF_ATTRIBUTE_TEMPLATES): vol.Schema({cv.string: cv.template}), - vol.Optional(ATTR_FRIENDLY_NAME): cv.string, - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_DELAY_ON): cv.positive_time_period, - vol.Optional(CONF_DELAY_OFF): cv.positive_time_period, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } +SENSOR_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, + vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTE_TEMPLATES): vol.Schema( + {cv.string: cv.template} + ), + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DELAY_ON): cv.positive_time_period, + vol.Optional(CONF_DELAY_OFF): cv.positive_time_period, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 1f9988bcafa..e0a1b2bc33c 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -66,6 +66,7 @@ TILT_FEATURES = ( ) COVER_SCHEMA = vol.All( + cv.deprecated(CONF_ENTITY_ID), vol.Schema( { vol.Inclusive(OPEN_ACTION, CONF_OPEN_OR_CLOSE): cv.SCRIPT_SCHEMA, diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 8017bef163b..4f75faa36ff 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -55,25 +55,28 @@ _VALID_STATES = [STATE_ON, STATE_OFF] _VALID_OSC = [True, False] _VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] -FAN_SCHEMA = vol.Schema( - { - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_SPEED_TEMPLATE): cv.template, - vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, - vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, - vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional( - CONF_SPEED_LIST, default=[SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - ): cv.ensure_list, - vol.Optional(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } +FAN_SCHEMA = vol.All( + cv.deprecated(CONF_ENTITY_ID), + vol.Schema( + { + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, + vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional( + CONF_SPEED_LIST, default=[SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + ): cv.ensure_list, + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ), ) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index a69c148ea8f..5f87301cce8 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -51,26 +51,29 @@ CONF_COLOR_ACTION = "set_color" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" CONF_WHITE_VALUE_ACTION = "set_white_value" -LIGHT_SCHEMA = vol.Schema( - { - vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_ICON_TEMPLATE): cv.template, - vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, - vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_LEVEL_TEMPLATE): cv.template, - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Optional(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_COLOR_TEMPLATE): cv.template, - vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_WHITE_VALUE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } +LIGHT_SCHEMA = vol.All( + cv.deprecated(CONF_ENTITY_ID), + vol.Schema( + { + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, + vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, + vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_LEVEL_TEMPLATE): cv.template, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template, + vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_COLOR_TEMPLATE): cv.template, + vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_WHITE_VALUE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 01c13af0ef2..754b3da27ac 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -34,22 +34,25 @@ CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" _LOGGER = logging.getLogger(__name__) -SENSOR_SCHEMA = vol.Schema( - { - vol.Required(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_ICON_TEMPLATE): cv.template, - vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, - vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, - vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( - {cv.string: cv.template} - ), - vol.Optional(ATTR_FRIENDLY_NAME): cv.string, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } +SENSOR_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, + vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( + {cv.string: cv.template} + ), + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index c612d3307da..fc7b2408f21 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -36,18 +36,21 @@ _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] ON_ACTION = "turn_on" OFF_ACTION = "turn_off" -SWITCH_SCHEMA = vol.Schema( - { - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_ICON_TEMPLATE): cv.template, - vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, - vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(ATTR_FRIENDLY_NAME): cv.string, - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } +SWITCH_SCHEMA = vol.All( + cv.deprecated(ATTR_ENTITY_ID), + vol.Schema( + { + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_ICON_TEMPLATE): cv.template, + vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, + vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 615995758a1..5bf8148b96e 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -65,27 +65,30 @@ _VALID_STATES = [ STATE_ERROR, ] -VACUUM_SCHEMA = vol.Schema( - { - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, - vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, - vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, - vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( - {cv.string: cv.template} - ), - vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, - vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, - vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, - vol.Optional(SERVICE_RETURN_TO_BASE): cv.SCRIPT_SCHEMA, - vol.Optional(SERVICE_CLEAN_SPOT): cv.SCRIPT_SCHEMA, - vol.Optional(SERVICE_LOCATE): cv.SCRIPT_SCHEMA, - vol.Optional(SERVICE_SET_FAN_SPEED): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_FAN_SPEED_LIST, default=[]): cv.ensure_list, - vol.Optional(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_UNIQUE_ID): cv.string, - } +VACUUM_SCHEMA = vol.All( + cv.deprecated(CONF_ENTITY_ID), + vol.Schema( + { + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, + vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( + {cv.string: cv.template} + ), + vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_RETURN_TO_BASE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_CLEAN_SPOT): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_LOCATE): cv.SCRIPT_SCHEMA, + vol.Optional(SERVICE_SET_FAN_SPEED): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_FAN_SPEED_LIST, default=[]): cv.ensure_list, + vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ), ) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( From cf4b6307ae177d50eca856d6c5c6e6df06963e66 Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Wed, 2 Sep 2020 00:16:40 +1000 Subject: [PATCH 550/862] Provide compatibility with older Home Assistant installations. (#39539) Prior 0.113 all lights and switches were reported as dimmable devices and this was fixed by #37978. However, under some circumstances Alexa will not smoothly transition from those broken devices to the new ones as it cache the list of entities. It is imperative to have Alexa rediscover all devices and then remove the now non-responding duplicates using the Alexa phone App. This can take quite a while if you have lots of devices. An alternative would be to log to the Alexa web site and remove all the lights instead and then re-discover them all. If you have multiple echo devices on your network, it is possible that the entries would continue to show as duplicates. This is due to an individual echo devices caching the old list and re-using it. The only known solution for this is to remove your echo devices from your Amazon account and re-add them. After that, have Alexa rediscover all your devices. This is a one-off requirement. Fixes #39503 --- .../components/emulated_hue/__init__.py | 9 ++++++ .../components/emulated_hue/hue_api.py | 8 ++++- tests/components/emulated_hue/test_hue_api.py | 32 ++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 5e50c727728..b4a49c7efcd 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -37,6 +37,7 @@ CONF_ENTITY_NAME = "name" CONF_EXPOSE_BY_DEFAULT = "expose_by_default" CONF_EXPOSED_DOMAINS = "exposed_domains" CONF_HOST_IP = "host_ip" +CONF_LIGHTS_ALL_DIMMABLE = "lights_all_dimmable" CONF_LISTEN_PORT = "listen_port" CONF_OFF_MAPS_TO_ON_DOMAINS = "off_maps_to_on_domains" CONF_TYPE = "type" @@ -45,6 +46,7 @@ CONF_UPNP_BIND_MULTICAST = "upnp_bind_multicast" TYPE_ALEXA = "alexa" TYPE_GOOGLE = "google_home" +DEFAULT_LIGHTS_ALL_DIMMABLE = False DEFAULT_LISTEN_PORT = 8300 DEFAULT_UPNP_BIND_MULTICAST = True DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ["script", "scene"] @@ -84,6 +86,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_ENTITIES): vol.Schema( {cv.entity_id: CONFIG_ENTITY_SCHEMA} ), + vol.Optional( + CONF_LIGHTS_ALL_DIMMABLE, default=DEFAULT_LIGHTS_ALL_DIMMABLE + ): cv.boolean, } ) }, @@ -244,6 +249,10 @@ class Config: if hidden_value is not None: self._entities_with_hidden_attr_in_config[entity_id] = hidden_value + # Get whether all non-dimmable lights should be reported as dimmable + # for compatibility with older installations. + self.lights_all_dimmable = conf.get(CONF_LIGHTS_ALL_DIMMABLE) + def entity_id_to_number(self, entity_id): """Get a unique number for the entity id.""" if self.type == TYPE_ALEXA: diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 0dd3d212dad..51580a28adf 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -774,12 +774,18 @@ def entity_to_json(config, entity): retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) - else: + elif not config.lights_all_dimmable: # On/Off light (ZigBee Device ID: 0x0000) # Supports groups, scenes and on/off control retval["type"] = "On/Off light" retval["productname"] = "On/Off light" retval["modelid"] = "HASS321" + else: + # Dimmable light (Zigbee Device ID: 0x0100) + # Supports groups, scenes, on/off and dimming + # Reports fixed brightness for compatibility with Alexa. + retval["type"] = "Dimmable light" + retval["modelid"] = "HASS123" retval["state"].update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX}) return retval diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 09791f9d242..0f61178107d 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -283,7 +283,37 @@ async def test_light_without_brightness_supported(hass_hue, hue_client): assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True assert light_without_brightness_json["type"] == "On/Off light" - # BRI required for alexa compat + +async def test_lights_all_dimmable(hass, aiohttp_client): + """Test CONF_LIGHTS_ALL_DIMMABLE.""" + # create a lamp without brightness support + hass.states.async_set("light.no_brightness", "on", {}) + await setup.async_setup_component( + hass, http.DOMAIN, {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}} + ) + await hass.async_block_till_done() + hue_config = { + emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT, + emulated_hue.CONF_EXPOSE_BY_DEFAULT: True, + emulated_hue.CONF_LIGHTS_ALL_DIMMABLE: True, + } + with patch("homeassistant.components.emulated_hue.create_upnp_datagram_endpoint"): + await setup.async_setup_component( + hass, + emulated_hue.DOMAIN, + {emulated_hue.DOMAIN: hue_config}, + ) + await hass.async_block_till_done() + config = Config(None, hue_config) + config.numbers = ENTITY_IDS_BY_NUMBER + web_app = hass.http.app + HueOneLightStateView(config).register(web_app, web_app.router) + client = await aiohttp_client(web_app) + light_without_brightness_json = await perform_get_light_state( + client, "light.no_brightness", HTTP_OK + ) + assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True + assert light_without_brightness_json["type"] == "Dimmable light" assert ( light_without_brightness_json["state"][HUE_API_STATE_BRI] == HUE_API_STATE_BRI_MAX From c6805aa3548d176e9f29c88abe4695c2df5ee233 Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 1 Sep 2020 17:41:05 +0300 Subject: [PATCH 551/862] Update pycoolmaster-async to 0.1.1 (#39551) --- homeassistant/components/coolmaster/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 513dc495da3..58bd51fca4d 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -3,6 +3,6 @@ "name": "CoolMasterNet", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", - "requirements": ["pycoolmasternet-async==0.1.0"], + "requirements": ["pycoolmasternet-async==0.1.1"], "codeowners": ["@OnFreund"] } diff --git a/requirements_all.txt b/requirements_all.txt index e1dfedf8b92..b69107257f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1270,7 +1270,7 @@ pycocotools==2.0.1 pycomfoconnect==0.3 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.0 +pycoolmasternet-async==0.1.1 # homeassistant.components.avri pycountry==19.8.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f9db1ad90a..d121b264046 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ pybotvac==0.0.17 pychromecast==7.2.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.0 +pycoolmasternet-async==0.1.1 # homeassistant.components.avri pycountry==19.8.18 From 064d115ccbe30a19839af6bd1b606211a661ceab Mon Sep 17 00:00:00 2001 From: Andrew Marks Date: Tue, 1 Sep 2020 11:51:27 -0400 Subject: [PATCH 552/862] Address open review issues in sharkiq integration (#39504) --- homeassistant/components/sharkiq/__init__.py | 13 +++---- .../components/sharkiq/config_flow.py | 23 ++++++------ .../components/sharkiq/manifest.json | 1 - homeassistant/components/sharkiq/strings.json | 11 +++++- .../components/sharkiq/translations/en.json | 9 ++++- .../components/sharkiq/update_coordinator.py | 15 ++++---- homeassistant/components/sharkiq/vacuum.py | 34 ++++++++---------- tests/components/sharkiq/test_config_flow.py | 2 -- tests/components/sharkiq/test_shark_iq.py | 36 ++++++------------- 9 files changed, 66 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index 09e6f4e6899..fb61e54f98f 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -10,7 +10,6 @@ from sharkiqpy import ( SharkIqNotAuthedError, get_ayla_api, ) -import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -18,8 +17,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import API_TIMEOUT, COMPONENTS, DOMAIN, LOGGER from .update_coordinator import SharkIqUpdateCoordinator -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" @@ -28,8 +25,7 @@ class CannotConnect(exceptions.HomeAssistantError): async def async_setup(hass, config): """Set up the sharkiq environment.""" hass.data.setdefault(DOMAIN, {}) - if DOMAIN not in config: - return True + return True async def async_connect_or_timeout(ayla_api: AylaApi) -> bool: @@ -38,11 +34,11 @@ async def async_connect_or_timeout(ayla_api: AylaApi) -> bool: with async_timeout.timeout(API_TIMEOUT): LOGGER.debug("Initialize connection to Ayla networks API") await ayla_api.async_sign_in() - except SharkIqAuthError as exc: - LOGGER.error("Authentication error connecting to Shark IQ api", exc_info=exc) + except SharkIqAuthError: + LOGGER.error("Authentication error connecting to Shark IQ api") return False except asyncio.TimeoutError as exc: - LOGGER.error("Timeout expired", exc_info=exc) + LOGGER.error("Timeout expired") raise CannotConnect from exc return True @@ -90,7 +86,6 @@ async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator): await coordinator.ayla_api.async_sign_out() except (SharkIqAuthError, SharkIqAuthExpiringError, SharkIqNotAuthedError): pass - return True async def async_update_options(hass, config_entry): diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 34328efc26d..4b2e54d3e38 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -50,17 +50,16 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} info = None - if user_input is not None: - # noinspection PyBroadException - try: - info = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + # noinspection PyBroadException + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" return info, errors async def async_step_user(self, user_input: Optional[Dict] = None): @@ -69,6 +68,8 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: info, errors = await self._async_validate_input(user_input) if info: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( diff --git a/homeassistant/components/sharkiq/manifest.json b/homeassistant/components/sharkiq/manifest.json index 8aa734ce28a..ee98ccfe32e 100644 --- a/homeassistant/components/sharkiq/manifest.json +++ b/homeassistant/components/sharkiq/manifest.json @@ -4,6 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sharkiq", "requirements": ["sharkiqpy==0.1.8"], - "dependencies": [], "codeowners": ["@ajmarks"] } diff --git a/homeassistant/components/sharkiq/strings.json b/homeassistant/components/sharkiq/strings.json index fe1a2125529..114087697ad 100644 --- a/homeassistant/components/sharkiq/strings.json +++ b/homeassistant/components/sharkiq/strings.json @@ -6,6 +6,12 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -14,7 +20,10 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "reauth_successful": "[%key:common::config_flow::data::access_token%] updated successfully", + "unknown": "[%key:common::config_flow::error::unknown%]" } } } diff --git a/homeassistant/components/sharkiq/translations/en.json b/homeassistant/components/sharkiq/translations/en.json index 331c12402f1..395c8b68e66 100644 --- a/homeassistant/components/sharkiq/translations/en.json +++ b/homeassistant/components/sharkiq/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured_account": "Account is already configured" + "already_configured_account": "Account is already configured", + "reauth_successful": "Reauthentication successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,12 @@ "unknown": "Unexpected error" }, "step": { + "reauth": { + "data": { + "password": "Password", + "username": "Username" + } + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index dff3681bba7..2b3f6070f3a 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -1,5 +1,6 @@ """Data update coordinator for shark iq vacuums.""" +import asyncio from typing import Dict, List, Set from async_timeout import timeout @@ -30,7 +31,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): ) -> None: """Set up the SharkIqUpdateCoordinator class.""" self.ayla_api = ayla_api - self.shark_vacs: Dict[SharkIqVacuum] = { + self.shark_vacs: Dict[str, SharkIqVacuum] = { sharkiq.serial_number: sharkiq for sharkiq in shark_vacs } self._config_entry = config_entry @@ -51,7 +52,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): async def _async_update_vacuum(sharkiq: SharkIqVacuum) -> None: """Asynchronously update the data for a single vacuum.""" dsn = sharkiq.serial_number - LOGGER.info("Updating sharkiq data for device DSN %s", dsn) + LOGGER.debug("Updating sharkiq data for device DSN %s", dsn) with timeout(API_TIMEOUT): await sharkiq.async_update() @@ -65,15 +66,15 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): if v["connection_status"] == "Online" and v["dsn"] in self.shark_vacs } - LOGGER.info("Updating sharkiq data") - for dsn in self._online_dsns: - await self._async_update_vacuum(self.shark_vacs[dsn]) + LOGGER.debug("Updating sharkiq data") + online_vacs = (self.shark_vacs[dsn] for dsn in self.online_dsns) + await asyncio.gather(*[self._async_update_vacuum(v) for v in online_vacs]) except ( SharkIqAuthError, SharkIqNotAuthedError, SharkIqAuthExpiringError, ) as err: - LOGGER.exception("Bad auth state", exc_info=err) + LOGGER.exception("Bad auth state") flow_context = { "source": "reauth", "unique_id": self._config_entry.unique_id, @@ -96,7 +97,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): raise UpdateFailed(err) from err except Exception as err: # pylint: disable=broad-except - LOGGER.exception("Unexpected error updating SharkIQ", exc_info=err) + LOGGER.exception("Unexpected error updating SharkIQ") raise UpdateFailed(err) from err return True diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index f4549e9eb35..96e4d98f318 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -66,16 +66,25 @@ ATTR_RECHARGE_RESUME = "recharge_and_resume" ATTR_RSSI = "rssi" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Shark IQ vacuum cleaner.""" + coordinator: SharkIqUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + devices: Iterable["SharkIqVacuum"] = coordinator.shark_vacs.values() + device_names = [d.name for d in devices] + LOGGER.debug( + "Found %d Shark IQ device(s): %s", + len(device_names), + ", ".join([d.name for d in devices]), + ) + async_add_entities([SharkVacuumEntity(d, coordinator) for d in devices]) + + class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): """Shark IQ vacuum entity.""" def __init__(self, sharkiq: SharkIqVacuum, coordinator: SharkIqUpdateCoordinator): """Create a new SharkVacuumEntity.""" super().__init__(coordinator) - if sharkiq.serial_number not in coordinator.shark_vacs: - raise RuntimeError( - f"Shark IQ robot {sharkiq.serial_number} is not known to the coordinator" - ) self.sharkiq = sharkiq def clean_spot(self, **kwargs): @@ -163,8 +172,6 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): In the app, these are (usually) handled by showing the robot as stopped and sending the user a notification. """ - if self.recharging_to_resume: - return STATE_RECHARGING_TO_RESUME if self.is_docked: return STATE_DOCKED return self.operating_mode @@ -229,7 +236,7 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): @property def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" - return list(FAN_SPEEDS_MAP.keys()) + return list(FAN_SPEEDS_MAP) # Various attributes we want to expose @property @@ -257,16 +264,3 @@ class SharkVacuumEntity(CoordinatorEntity, StateVacuumEntity): ATTR_RECHARGE_RESUME: self.recharge_resume, } return data - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Shark IQ vacuum cleaner.""" - coordinator: SharkIqUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - devices: Iterable["SharkIqVacuum"] = coordinator.shark_vacs.values() - device_names = [d.name for d in devices] - LOGGER.debug( - "Found %d Shark IQ device(s): %s", - len(device_names), - ", ".join([d.name for d in devices]), - ) - async_add_entities([SharkVacuumEntity(d, coordinator) for d in devices]) diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index 0d1612f857b..f588b5f82a2 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -38,7 +38,6 @@ async def test_form(hass): result["flow_id"], CONFIG, ) - await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == f"{TEST_USERNAME:s}" @@ -118,7 +117,6 @@ async def test_reauth(hass): ), patch("sharkiqpy.AylaApi.async_sign_in", return_value=True): mock_config = MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG) mock_config.add_to_hass(hass) - hass.config_entries.async_update_entry(mock_config, data=CONFIG) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG diff --git a/tests/components/sharkiq/test_shark_iq.py b/tests/components/sharkiq/test_shark_iq.py index 48f623d4509..927f62b138c 100644 --- a/tests/components/sharkiq/test_shark_iq.py +++ b/tests/components/sharkiq/test_shark_iq.py @@ -1,6 +1,7 @@ """Test the Shark IQ vacuum entity.""" from copy import deepcopy import enum +import json from typing import Dict, List from sharkiqpy import AylaApi, Properties, SharkIqAuthError, SharkIqVacuum, get_ayla_api @@ -11,7 +12,6 @@ from homeassistant.components.sharkiq.vacuum import ( ATTR_ERROR_MSG, ATTR_LOW_LIGHT, ATTR_RECHARGE_RESUME, - STATE_RECHARGING_TO_RESUME, SharkVacuumEntity, ) from homeassistant.components.vacuum import ( @@ -44,12 +44,6 @@ from .const import ( from tests.async_mock import MagicMock, patch -try: - import ujson as json -except ImportError: - import json - - MockAyla = MagicMock(spec=AylaApi) # pylint: disable=invalid-name @@ -109,7 +103,7 @@ async def test_shark_operation_modes(hass: HomeAssistant) -> None: shark.sharkiq.set_property_value(Properties.DOCKED_STATUS, 1) assert isinstance(shark.is_docked, bool) and shark.is_docked assert isinstance(shark.recharging_to_resume, bool) and shark.recharging_to_resume - assert shark.state == STATE_RECHARGING_TO_RESUME + assert shark.state == STATE_DOCKED shark.sharkiq.set_property_value(Properties.RECHARGING_TO_RESUME, 0) assert shark.state == STATE_DOCKED @@ -175,9 +169,8 @@ async def test_shark_metadata(hass: HomeAssistant) -> None: "model": "RV1001AE", "sw_version": "Dummy Firmware 1.0", } - state_json = json.dumps(shark.device_info, sort_keys=True) - target_json = json.dumps(target_device_info, sort_keys=True) - assert state_json == target_json + + assert shark.device_info == target_device_info def _get_async_update(err=None): @@ -225,29 +218,20 @@ async def test_coordinator_match(hass: HomeAssistant): coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac1]) - # The first should succeed, the second should fail - api1 = SharkVacuumEntity(shark_vac1, coordinator) - try: - _ = SharkVacuumEntity(shark_vac2, coordinator) - except RuntimeError: - api2_failed = True - else: - api2_failed = False - assert api2_failed - + api = SharkVacuumEntity(shark_vac1, coordinator) coordinator.last_update_success = True coordinator._online_dsns = set() # pylint: disable=protected-access - assert not api1.is_online - assert not api1.available + assert not api.is_online + assert not api.available coordinator._online_dsns = { # pylint: disable=protected-access shark_vac1.serial_number } - assert api1.is_online - assert api1.available + assert api.is_online + assert api.available coordinator.last_update_success = False - assert not api1.available + assert not api.available async def test_simple_properties(hass: HomeAssistant): From 1eda3d31f98e0c3d2bd4bdde8ad91cb2354544b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20=C5=9EEREMET?= Date: Tue, 1 Sep 2020 20:42:17 +0300 Subject: [PATCH 553/862] Apply code review on progettihwsw (#39520) --- .../components/progettihwsw/binary_sensor.py | 10 +- .../components/progettihwsw/config_flow.py | 67 +++++++------ .../components/progettihwsw/strings.json | 6 +- .../components/progettihwsw/switch.py | 9 +- .../progettihwsw/translations/en.json | 6 +- .../progettihwsw/test_config_flow.py | 97 ++++++------------- 6 files changed, 80 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/progettihwsw/binary_sensor.py b/homeassistant/components/progettihwsw/binary_sensor.py index e6ab49e8e5c..1331d3abb0a 100644 --- a/homeassistant/components/progettihwsw/binary_sensor.py +++ b/homeassistant/components/progettihwsw/binary_sensor.py @@ -18,12 +18,6 @@ from .const import DEFAULT_POLLING_INTERVAL_SEC, DOMAIN _LOGGER = logging.getLogger(DOMAIN) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set the progettihwsw platform up and create sensor instances (legacy).""" - - return True - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the binary sensors from a config entry.""" board_api = hass.data[DOMAIN][config_entry.entry_id] @@ -47,9 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for i in range(1, int(input_count) + 1): binary_sensors.append( ProgettihwswBinarySensor( - hass, coordinator, - config_entry, f"Input #{i}", setup_input(board_api, i), ) @@ -61,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class ProgettihwswBinarySensor(CoordinatorEntity, BinarySensorEntity): """Represent a binary sensor.""" - def __init__(self, hass, coordinator, config_entry, name, sensor: Input): + def __init__(self, coordinator, name, sensor: Input): """Set initializing values.""" super().__init__(coordinator) self._name = name diff --git a/homeassistant/components/progettihwsw/config_flow.py b/homeassistant/components/progettihwsw/config_flow.py index 9306b134dbb..e65b58b370a 100644 --- a/homeassistant/components/progettihwsw/config_flow.py +++ b/homeassistant/components/progettihwsw/config_flow.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions -from .const import DOMAIN # pylint: disable=unused-import +from .const import DOMAIN DATA_SCHEMA = vol.Schema( {vol.Required("host"): str, vol.Required("port", default=80): int} @@ -15,10 +15,20 @@ DATA_SCHEMA = vol.Schema( async def validate_input(hass: core.HomeAssistant, data): """Validate the user host input.""" + confs = hass.config_entries.async_entries(DOMAIN) + same_entries = [ + True + for entry in confs + if entry.data["host"] == data["host"] and entry.data["port"] == data["port"] + ] + + if same_entries: + raise ExistingEntry + api_instance = ProgettiHWSWAPI(f'{data["host"]}:{data["port"]}') is_valid = await api_instance.check_board() - if is_valid is False: + if not is_valid: raise CannotConnect return { @@ -29,43 +39,36 @@ async def validate_input(hass: core.HomeAssistant, data): } -async def validate_input_relay_modes(data): - """Validate the user input in relay modes form.""" - for mode in data.values(): - if mode not in ("bistable", "monostable"): - raise WrongInfo - - return True - - class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for ProgettiHWSW Automation.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + def __init__(self): + """Initialize class variables.""" + self.s1_in = None + async def async_step_relay_modes(self, user_input=None): """Manage relay modes step.""" errors = {} if user_input is not None: - try: - await validate_input_relay_modes(user_input) - whole_data = user_input - whole_data.update(self.s1_in) - except WrongInfo: - errors["base"] = "wrong_info_relay_modes" - except Exception: # pylint: disable=broad-except - errors["base"] = "unknown" - else: - return self.async_create_entry( - title=whole_data["title"], data=whole_data - ) + + whole_data = user_input + whole_data.update(self.s1_in) + + return self.async_create_entry(title=whole_data["title"], data=whole_data) relay_modes_schema = {} for i in range(1, int(self.s1_in["relay_count"]) + 1): relay_modes_schema[ vol.Required(f"relay_{str(i)}", default="bistable") - ] = str + ] = vol.In( + { + "bistable": "Bistable (ON/OFF Mode)", + "monostable": "Monostable (Timer Mode)", + } + ) return self.async_show_form( step_id="relay_modes", @@ -77,17 +80,19 @@ class ProgettiHWSWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: + try: info = await validate_input(self.hass, user_input) - user_input.update(info) - self.s1_in = ( # pylint: disable=attribute-defined-outside-init - user_input - ) - return await self.async_step_relay_modes() except CannotConnect: errors["base"] = "cannot_connect" + except ExistingEntry: + return self.async_abort(reason="already_configured") except Exception: # pylint: disable=broad-except errors["base"] = "unknown" + else: + user_input.update(info) + self.s1_in = user_input + return await self.async_step_relay_modes() return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors @@ -100,3 +105,7 @@ class CannotConnect(exceptions.HomeAssistantError): class WrongInfo(exceptions.HomeAssistantError): """Error to indicate we cannot validate relay modes input.""" + + +class ExistingEntry(exceptions.HomeAssistantError): + """Error to indicate we cannot validate relay modes input.""" diff --git a/homeassistant/components/progettihwsw/strings.json b/homeassistant/components/progettihwsw/strings.json index 2c25433fba9..9a0c49d2cba 100644 --- a/homeassistant/components/progettihwsw/strings.json +++ b/homeassistant/components/progettihwsw/strings.json @@ -2,8 +2,10 @@ "config": { "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]", - "wrong_info_relay_modes": "Relay mode selection must be monostable or bistable." + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "step": { "user": { diff --git a/homeassistant/components/progettihwsw/switch.py b/homeassistant/components/progettihwsw/switch.py index 5dc6005908f..1d4c1de9993 100644 --- a/homeassistant/components/progettihwsw/switch.py +++ b/homeassistant/components/progettihwsw/switch.py @@ -18,11 +18,6 @@ from .const import DEFAULT_POLLING_INTERVAL_SEC, DOMAIN _LOGGER = logging.getLogger(DOMAIN) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set the switch platform up (legacy).""" - return True - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the switches from a config entry.""" board_api = hass.data[DOMAIN][config_entry.entry_id] @@ -46,9 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for i in range(1, int(relay_count) + 1): switches.append( ProgettihwswSwitch( - hass, coordinator, - config_entry, f"Relay #{i}", setup_switch(board_api, i, config_entry.data[f"relay_{str(i)}"]), ) @@ -60,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class ProgettihwswSwitch(CoordinatorEntity, SwitchEntity): """Represent a switch entity.""" - def __init__(self, hass, coordinator, config_entry, name, switch: Relay): + def __init__(self, coordinator, name, switch: Relay): """Initialize the values.""" super().__init__(coordinator) self._switch = switch diff --git a/homeassistant/components/progettihwsw/translations/en.json b/homeassistant/components/progettihwsw/translations/en.json index 9add8609390..7ec64794c91 100644 --- a/homeassistant/components/progettihwsw/translations/en.json +++ b/homeassistant/components/progettihwsw/translations/en.json @@ -2,8 +2,10 @@ "config": { "error": { "cannot_connect": "Failed to connect", - "unknown": "Unexpected error", - "wrong_info_relay_modes": "Relay mode selection must be monostable or bistable." + "unknown": "Unknown error" + }, + "abort": { + "already_configured": "This board has already been set up." }, "step": { "relay_modes": { diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 7da0ef82642..7a0dbd692c0 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -2,9 +2,14 @@ from homeassistant import config_entries, setup from homeassistant.components.progettihwsw.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from tests.async_mock import patch +from tests.common import MockConfigEntry mock_value_step_user = { "title": "1R & 1IN Board", @@ -60,35 +65,6 @@ async def test_form(hass): assert result3["data"]["relay_count"] == result3["data"]["input_count"] == 1 -async def test_form_wrong_info(hass): - """Test we handle wrong info exception.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["step_id"] == "user" - - with patch( - "homeassistant.components.progettihwsw.config_flow.ProgettiHWSWAPI.check_board", - return_value=mock_value_step_user, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_HOST: "", CONF_PORT: 80} - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "relay_modes" - assert result2["errors"] == {} - - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], {"relay_1": ""} - ) - - assert result3["type"] == RESULT_TYPE_FORM - assert result3["step_id"] == "relay_modes" - assert result3["errors"] == {"base": "wrong_info_relay_modes"} - - async def test_form_cannot_connect(hass): """Test we handle unexisting board.""" result = await hass.config_entries.flow.async_init( @@ -111,6 +87,32 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} +async def test_form_existing_entry_exception(hass): + """Test we handle existing board.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "", + CONF_PORT: 80, + }, + ) + entry.add_to_hass(hass) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "", CONF_PORT: 80}, + ) + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + async def test_form_user_exception(hass): """Test we handle unknown exception.""" result = await hass.config_entries.flow.async_init( @@ -131,38 +133,3 @@ async def test_form_user_exception(hass): assert result2["type"] == RESULT_TYPE_FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} - - -async def test_form_rm_exception(hass): - """Test we handle unknown exception on seconds step.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["step_id"] == "user" - - with patch( - "homeassistant.components.progettihwsw.config_flow.ProgettiHWSWAPI.check_board", - return_value=mock_value_step_user, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "", CONF_PORT: 80}, - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "relay_modes" - assert result2["errors"] == {} - - with patch( - "homeassistant.components.progettihwsw.config_flow.validate_input_relay_modes", - side_effect=Exception, - ): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - {"relay_1": "bistable"}, - ) - - assert result3["type"] == RESULT_TYPE_FORM - assert result3["step_id"] == "relay_modes" - assert result3["errors"] == {"base": "unknown"} From 10606360f7cf2cd6b28e224611140596217f2f89 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Sep 2020 00:35:11 +0200 Subject: [PATCH 554/862] Updated frontend to 20200901.0 (#39560) --- 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 976f78afc4a..6e1836a5c63 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200824.0"], + "requirements": ["home-assistant-frontend==20200901.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c8ff36872e6..d88eab47364 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.35.0 -home-assistant-frontend==20200824.0 +home-assistant-frontend==20200901.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index b69107257f1..ec93f932b1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -743,7 +743,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200824.0 +home-assistant-frontend==20200901.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d121b264046..bafccebb99c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200824.0 +home-assistant-frontend==20200901.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5ce62c84469129440361e8c4a6456eaddf22a7c4 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 2 Sep 2020 00:03:29 +0000 Subject: [PATCH 555/862] [ci skip] Translation update --- .../accuweather/translations/fr.json | 9 +++ .../accuweather/translations/pt.json | 8 +++ .../components/airly/translations/fr.json | 2 +- .../components/august/translations/pt.json | 3 + .../automation/translations/fr.json | 2 +- .../azure_devops/translations/pt.json | 8 +++ .../binary_sensor/translations/fr.json | 4 +- .../components/blink/translations/pt.json | 4 ++ .../components/bond/translations/pt.json | 8 +++ .../components/broadlink/translations/pt.json | 21 ++++++ .../cert_expiry/translations/fr.json | 4 +- .../components/control4/translations/pt.json | 21 ++++++ .../components/daikin/translations/fr.json | 2 +- .../components/daikin/translations/pt.json | 3 +- .../components/directv/translations/pt.json | 3 + .../components/eafm/translations/fr.json | 16 +++++ .../components/eafm/translations/pt.json | 7 ++ .../components/elkm1/translations/pt.json | 3 + .../components/enocean/translations/pt.json | 3 + .../components/fan/translations/fr.json | 4 +- .../flick_electric/translations/pt.json | 3 + .../components/flo/translations/pt.json | 21 ++++++ .../components/flume/translations/pt.json | 3 + .../garmin_connect/translations/pt.json | 6 ++ .../components/griddy/translations/pt.json | 7 ++ .../components/harmony/translations/pt.json | 3 + .../components/hlk_sw16/translations/pt.json | 21 ++++++ .../translations/pt.json | 3 + .../components/iaqualink/translations/fr.json | 2 +- .../input_boolean/translations/fr.json | 4 +- .../components/insteon/translations/en.json | 2 +- .../components/insteon/translations/es.json | 3 +- .../components/insteon/translations/fr.json | 67 +++++++++++++++++-- .../components/insteon/translations/no.json | 3 +- .../components/insteon/translations/pl.json | 13 ++++ .../components/insteon/translations/pt.json | 28 +++++++- .../components/juicenet/translations/pt.json | 7 ++ .../components/kodi/translations/pt.json | 32 +++++++++ .../components/life360/translations/pt.json | 4 +- .../components/light/translations/fr.json | 2 +- .../logi_circle/translations/fr.json | 2 +- .../media_player/translations/fr.json | 4 +- .../components/melcloud/translations/pt.json | 3 + .../components/monoprice/translations/pt.json | 3 + .../components/myq/translations/pt.json | 3 + .../components/neato/translations/pt.json | 3 + .../components/netatmo/translations/fr.json | 2 +- .../components/netatmo/translations/pt.json | 6 +- .../components/nexia/translations/pt.json | 3 + .../nightscout/translations/pt.json | 11 +++ .../components/notify/translations/fr.json | 2 +- .../components/notion/translations/fr.json | 2 +- .../components/nuheat/translations/pt.json | 3 + .../components/nut/translations/pt.json | 3 + .../components/nws/translations/fr.json | 2 +- .../components/nzbget/translations/fr.json | 22 ++++++ .../components/nzbget/translations/pt.json | 8 +-- .../ovo_energy/translations/fr.json | 5 ++ .../ovo_energy/translations/pt.json | 16 +++++ .../components/plugwise/translations/pt.json | 7 ++ .../components/point/translations/fr.json | 10 +-- .../components/poolsense/translations/pt.json | 3 + .../components/powerwall/translations/pt.json | 7 ++ .../progettihwsw/translations/en.json | 9 +-- .../progettihwsw/translations/es.json | 3 +- .../progettihwsw/translations/fr.json | 33 +++++++++ .../progettihwsw/translations/pt.json | 2 +- .../progettihwsw/translations/ru.json | 3 + .../components/rachio/translations/pt.json | 14 ++++ .../components/remote/translations/fr.json | 2 +- .../components/rfxtrx/translations/pt.json | 7 ++ .../components/ring/translations/pt.json | 3 + .../components/risco/translations/es.json | 15 ++++- .../components/risco/translations/pt.json | 18 +++++ .../components/roku/translations/pt.json | 7 ++ .../components/roon/translations/pt.json | 18 +++++ .../components/sense/translations/pt.json | 3 + .../components/sentry/translations/pt.json | 10 +++ .../components/sharkiq/translations/en.json | 4 +- .../components/sharkiq/translations/es.json | 11 ++- .../components/sharkiq/translations/pt.json | 8 +-- .../components/sharkiq/translations/ru.json | 11 ++- .../components/shelly/translations/en.json | 15 +++-- .../components/shelly/translations/es.json | 7 ++ .../components/shelly/translations/pt.json | 4 +- .../components/shelly/translations/ru.json | 7 ++ .../simplisafe/translations/pt.json | 8 ++- .../components/smappee/translations/pt.json | 14 ++++ .../smart_meter_texas/translations/pt.json | 20 ++++++ .../components/solaredge/translations/fr.json | 2 +- .../components/solaredge/translations/pt.json | 11 +++ .../components/solarlog/translations/fr.json | 2 +- .../components/solarlog/translations/pt.json | 3 + .../components/spider/translations/pt.json | 19 ++++++ .../components/spotify/translations/es.json | 7 +- .../squeezebox/translations/pt.json | 3 + .../components/switch/translations/fr.json | 2 +- .../components/syncthru/translations/pt.json | 9 ++- .../synology_dsm/translations/fr.json | 4 +- .../components/tado/translations/pt.json | 3 + .../components/tesla/translations/fr.json | 2 +- .../components/timer/translations/fr.json | 6 +- .../twentemilieu/translations/pt.json | 7 ++ .../components/upb/translations/pt.json | 7 ++ .../components/vesync/translations/fr.json | 2 +- .../components/vizio/translations/pt.json | 2 + .../components/volumio/translations/pt.json | 7 ++ .../components/wilight/translations/pt.json | 2 +- .../components/wolflink/translations/pt.json | 20 ++++++ .../xiaomi_aqara/translations/pt.json | 8 +++ .../components/yeelight/translations/es.json | 39 +++++++++++ .../components/yeelight/translations/fr.json | 39 +++++++++++ .../components/yeelight/translations/no.json | 39 +++++++++++ .../components/yeelight/translations/pt.json | 39 +++++++++++ 114 files changed, 959 insertions(+), 80 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/fr.json create mode 100644 homeassistant/components/azure_devops/translations/pt.json create mode 100644 homeassistant/components/broadlink/translations/pt.json create mode 100644 homeassistant/components/control4/translations/pt.json create mode 100644 homeassistant/components/eafm/translations/fr.json create mode 100644 homeassistant/components/eafm/translations/pt.json create mode 100644 homeassistant/components/flo/translations/pt.json create mode 100644 homeassistant/components/griddy/translations/pt.json create mode 100644 homeassistant/components/hlk_sw16/translations/pt.json create mode 100644 homeassistant/components/insteon/translations/pl.json create mode 100644 homeassistant/components/juicenet/translations/pt.json create mode 100644 homeassistant/components/nightscout/translations/pt.json create mode 100644 homeassistant/components/nzbget/translations/fr.json create mode 100644 homeassistant/components/ovo_energy/translations/pt.json create mode 100644 homeassistant/components/plugwise/translations/pt.json create mode 100644 homeassistant/components/powerwall/translations/pt.json create mode 100644 homeassistant/components/progettihwsw/translations/fr.json create mode 100644 homeassistant/components/rachio/translations/pt.json create mode 100644 homeassistant/components/rfxtrx/translations/pt.json create mode 100644 homeassistant/components/roon/translations/pt.json create mode 100644 homeassistant/components/sentry/translations/pt.json create mode 100644 homeassistant/components/smappee/translations/pt.json create mode 100644 homeassistant/components/smart_meter_texas/translations/pt.json create mode 100644 homeassistant/components/solaredge/translations/pt.json create mode 100644 homeassistant/components/spider/translations/pt.json create mode 100644 homeassistant/components/twentemilieu/translations/pt.json create mode 100644 homeassistant/components/upb/translations/pt.json create mode 100644 homeassistant/components/wolflink/translations/pt.json create mode 100644 homeassistant/components/yeelight/translations/es.json create mode 100644 homeassistant/components/yeelight/translations/fr.json create mode 100644 homeassistant/components/yeelight/translations/no.json create mode 100644 homeassistant/components/yeelight/translations/pt.json diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json new file mode 100644 index 00000000000..5b69afc7334 --- /dev/null +++ b/homeassistant/components/accuweather/translations/fr.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/pt.json b/homeassistant/components/accuweather/translations/pt.json index 6288344fd6b..084965331e5 100644 --- a/homeassistant/components/accuweather/translations/pt.json +++ b/homeassistant/components/accuweather/translations/pt.json @@ -1,8 +1,16 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { "user": { "data": { + "api_key": "Chave de API", "latitude": "Latitude", "longitude": "Longitude" } diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json index a454a38fbe6..ac821ab226c 100644 --- a/homeassistant/components/airly/translations/fr.json +++ b/homeassistant/components/airly/translations/fr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Cl\u00e9 API Airly", + "api_key": "Cl\u00e9 d'API", "latitude": "Latitude", "longitude": "Longitude", "name": "Nom de l'int\u00e9gration" diff --git a/homeassistant/components/august/translations/pt.json b/homeassistant/components/august/translations/pt.json index fdb6f03c01f..5560383b710 100644 --- a/homeassistant/components/august/translations/pt.json +++ b/homeassistant/components/august/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/automation/translations/fr.json b/homeassistant/components/automation/translations/fr.json index 548c30fd0de..30426582414 100644 --- a/homeassistant/components/automation/translations/fr.json +++ b/homeassistant/components/automation/translations/fr.json @@ -5,5 +5,5 @@ "on": "Actif" } }, - "title": "Automation" + "title": "Automatisation" } \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/pt.json b/homeassistant/components/azure_devops/translations/pt.json new file mode 100644 index 00000000000..50d5409ef8d --- /dev/null +++ b/homeassistant/components/azure_devops/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Token de Acesso atualizado com sucesso" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant/components/binary_sensor/translations/fr.json index a27c3368923..fe7b0e19754 100644 --- a/homeassistant/components/binary_sensor/translations/fr.json +++ b/homeassistant/components/binary_sensor/translations/fr.json @@ -155,11 +155,11 @@ "on": "Dangereux" }, "smoke": { - "off": "RAS", + "off": "Non d\u00e9tect\u00e9", "on": "D\u00e9tect\u00e9" }, "sound": { - "off": "RAS", + "off": "Non d\u00e9tect\u00e9", "on": "D\u00e9tect\u00e9" }, "vibration": { diff --git a/homeassistant/components/blink/translations/pt.json b/homeassistant/components/blink/translations/pt.json index 12cdc94043e..188effb27af 100644 --- a/homeassistant/components/blink/translations/pt.json +++ b/homeassistant/components/blink/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_access_token": "Token de acesso inv\u00e1lido" + }, "step": { "2fa": { "description": "Digite o pin enviado para o seu email. Se o email n\u00e3o contiver um pin, deixe em branco" diff --git a/homeassistant/components/bond/translations/pt.json b/homeassistant/components/bond/translations/pt.json index 24a397bf001..2173d698932 100644 --- a/homeassistant/components/bond/translations/pt.json +++ b/homeassistant/components/bond/translations/pt.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { + "confirm": { + "data": { + "access_token": "Token de Acesso" + } + }, "user": { "data": { "access_token": "Token de Acesso", diff --git a/homeassistant/components/broadlink/translations/pt.json b/homeassistant/components/broadlink/translations/pt.json new file mode 100644 index 00000000000..c1b7a83d443 --- /dev/null +++ b/homeassistant/components/broadlink/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} ({model} em {host})", + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/fr.json b/homeassistant/components/cert_expiry/translations/fr.json index 8c3d92edf92..070b5e26cba 100644 --- a/homeassistant/components/cert_expiry/translations/fr.json +++ b/homeassistant/components/cert_expiry/translations/fr.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "host": "Le nom d'h\u00f4te du certificat", + "host": "H\u00f4te", "name": "Le nom du certificat", - "port": "Le port du certificat" + "port": "Port" }, "title": "D\u00e9finir le certificat \u00e0 tester" } diff --git a/homeassistant/components/control4/translations/pt.json b/homeassistant/components/control4/translations/pt.json new file mode 100644 index 00000000000..5f67ee940b1 --- /dev/null +++ b/homeassistant/components/control4/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Endere\u00e7o IP", + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/fr.json b/homeassistant/components/daikin/translations/fr.json index 7711b8bc045..75f3260a86c 100644 --- a/homeassistant/components/daikin/translations/fr.json +++ b/homeassistant/components/daikin/translations/fr.json @@ -16,7 +16,7 @@ "key": "Cl\u00e9 d'authentification (utilis\u00e9e uniquement par les appareils BRP072C/Zena)", "password": "Mot de passe de l'appareil (utilis\u00e9 uniquement par les appareils SKYFi)" }, - "description": "Entrez l'adresse IP de votre Daikin AC.", + "description": "Saisissez l'adresse IP de votre Daikin AC. \n\n Notez que Cl\u00e9 d'API et Mot de passe sont utilis\u00e9s respectivement par les p\u00e9riph\u00e9riques BRP072Cxx et SKYFi.", "title": "Configurer Daikin AC" } } diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json index 31cd6001151..617aed245e0 100644 --- a/homeassistant/components/daikin/translations/pt.json +++ b/homeassistant/components/daikin/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { "user": { diff --git a/homeassistant/components/directv/translations/pt.json b/homeassistant/components/directv/translations/pt.json index ce7cbc3f548..96a09567650 100644 --- a/homeassistant/components/directv/translations/pt.json +++ b/homeassistant/components/directv/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/eafm/translations/fr.json b/homeassistant/components/eafm/translations/fr.json new file mode 100644 index 00000000000..ca9d788e75d --- /dev/null +++ b/homeassistant/components/eafm/translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "no_stations": "Aucune station de surveillance des inondations n'a \u00e9t\u00e9 trouv\u00e9e." + }, + "step": { + "user": { + "data": { + "station": "Station" + }, + "description": "S\u00e9lectionnez la station que vous souhaitez surveiller", + "title": "Suivre une station de surveillance des inondations" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/pt.json b/homeassistant/components/eafm/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/eafm/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/pt.json b/homeassistant/components/elkm1/translations/pt.json index 83e574aa2e2..2f61dbc37e0 100644 --- a/homeassistant/components/elkm1/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/enocean/translations/pt.json b/homeassistant/components/enocean/translations/pt.json index a004e5cae8b..4bca8a1319e 100644 --- a/homeassistant/components/enocean/translations/pt.json +++ b/homeassistant/components/enocean/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "flow_title": "Configura\u00e7\u00e3o ENOcean" }, "title": "EnOcean" diff --git a/homeassistant/components/fan/translations/fr.json b/homeassistant/components/fan/translations/fr.json index 88b4057a3eb..6dc4aef56e4 100644 --- a/homeassistant/components/fan/translations/fr.json +++ b/homeassistant/components/fan/translations/fr.json @@ -15,8 +15,8 @@ }, "state": { "_": { - "off": "\u00c9teint", - "on": "Marche" + "off": "Inactif", + "on": "Actif" } }, "title": "Ventilateur" diff --git a/homeassistant/components/flick_electric/translations/pt.json b/homeassistant/components/flick_electric/translations/pt.json index b8a454fbaba..1e3d9138c84 100644 --- a/homeassistant/components/flick_electric/translations/pt.json +++ b/homeassistant/components/flick_electric/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flo/translations/pt.json b/homeassistant/components/flo/translations/pt.json new file mode 100644 index 00000000000..561c8d77287 --- /dev/null +++ b/homeassistant/components/flo/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/pt.json b/homeassistant/components/flume/translations/pt.json index b4642359973..4a071063d47 100644 --- a/homeassistant/components/flume/translations/pt.json +++ b/homeassistant/components/flume/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/garmin_connect/translations/pt.json b/homeassistant/components/garmin_connect/translations/pt.json index b4642359973..b3b468ff3ec 100644 --- a/homeassistant/components/garmin_connect/translations/pt.json +++ b/homeassistant/components/garmin_connect/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/griddy/translations/pt.json b/homeassistant/components/griddy/translations/pt.json new file mode 100644 index 00000000000..0c5c7760566 --- /dev/null +++ b/homeassistant/components/griddy/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/pt.json b/homeassistant/components/harmony/translations/pt.json index ce7cbc3f548..2a9c91681be 100644 --- a/homeassistant/components/harmony/translations/pt.json +++ b/homeassistant/components/harmony/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hlk_sw16/translations/pt.json b/homeassistant/components/hlk_sw16/translations/pt.json new file mode 100644 index 00000000000..561c8d77287 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pt.json b/homeassistant/components/hunterdouglas_powerview/translations/pt.json index f7dc708a2d6..8b7889f0d12 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/pt.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/iaqualink/translations/fr.json b/homeassistant/components/iaqualink/translations/fr.json index 60e9634acce..47ed27fb339 100644 --- a/homeassistant/components/iaqualink/translations/fr.json +++ b/homeassistant/components/iaqualink/translations/fr.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Nom d'utilisateur / adresse e-mail" + "username": "Nom d'utilisateur" }, "description": "Veuillez saisir le nom d'utilisateur et le mot de passe de votre compte iAqualink.", "title": "Se connecter \u00e0 iAqualink" diff --git a/homeassistant/components/input_boolean/translations/fr.json b/homeassistant/components/input_boolean/translations/fr.json index 5bd2cf2891c..1d83af839c1 100644 --- a/homeassistant/components/input_boolean/translations/fr.json +++ b/homeassistant/components/input_boolean/translations/fr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Arr\u00eat\u00e9", - "on": "Marche" + "off": "Inactif", + "on": "Actif" } }, "title": "Entr\u00e9e logique" diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json index eec80f60b90..5b5ba8a8366 100644 --- a/homeassistant/components/insteon/translations/en.json +++ b/homeassistant/components/insteon/translations/en.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "An Insteon modem connection is already configured", "cannot_connect": "Failed to connect", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index bf38bb38d94..40b87768cd4 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Una conexi\u00f3n de m\u00f3dem Insteon ya est\u00e1 configurada", - "cannot_connect": "No se puede conectar al m\u00f3dem Insteon" + "cannot_connect": "No se puede conectar al m\u00f3dem Insteon", + "single_instance_allowed": "Ya esta configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "cannot_connect": "No se conect\u00f3 al m\u00f3dem Insteon, por favor, int\u00e9ntelo de nuevo.", diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index 4962f03e621..88ddbb064f8 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -1,13 +1,72 @@ { "config": { + "abort": { + "already_configured": "Une connexion Insteon par modem est d\u00e9j\u00e0 configur\u00e9e", + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "select_single": "S\u00e9lectionnez une option." + }, "step": { + "hub1": { + "data": { + "host": "Adresse IP du hub", + "port": "Port IP" + }, + "description": "Configurer le Hub Insteon Version 1 (avant 2014).", + "title": "Hub Insteon Version 1" + }, + "hub2": { + "data": { + "host": "Adresse IP du hub", + "password": "Mot de passe", + "port": "Port IP", + "username": "Nom d'utilisateur" + }, + "description": "Configurez le Hub Insteon version 2.", + "title": "Hub Insteon Version 2" + }, + "hubv1": { + "data": { + "host": "Adresse IP", + "port": "Port" + }, + "description": "Configurer le Hub Insteon Version 1 (avant 2014).", + "title": "Hub Insteon Version 1" + }, + "hubv2": { + "data": { + "host": "Adresse IP", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "description": "Configurez le Hub Insteon version 2.", + "title": "Hub Insteon Version 2" + }, "init": { + "data": { + "hubv1": "Hub Version 1 (avant 2014)" + }, + "title": "Insteon" + }, + "plm": { + "data": { + "device": "Chemin du p\u00e9riph\u00e9rique USB" + } + }, + "user": { + "data": { + "modem_type": "Type de modem." + }, "title": "Insteon" } } }, "options": { "error": { + "cannot_connect": "\u00c9chec de connexion", "select_single": "S\u00e9lectionnez une option" }, "step": { @@ -22,10 +81,10 @@ }, "change_hub_config": { "data": { - "host": "Nouveau nom d\u2019h\u00f4te ou adresse IP", - "password": "Nouveau mot de passe", - "port": "Nouveau num\u00e9ro de port", - "username": "Nouveau nom d\u2019utilisateur" + "host": "Adresse IP", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" }, "title": "Insteon" }, diff --git a/homeassistant/components/insteon/translations/no.json b/homeassistant/components/insteon/translations/no.json index 4dfaa95a20e..09a16e67162 100644 --- a/homeassistant/components/insteon/translations/no.json +++ b/homeassistant/components/insteon/translations/no.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "En Insteon-modemtilkobling er allerede konfigurert", - "cannot_connect": "Tilkobling mislyktes." + "cannot_connect": "Tilkobling mislyktes.", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { "cannot_connect": "Tilkobling mislyktes.", diff --git a/homeassistant/components/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json new file mode 100644 index 00000000000..baa541c74f2 --- /dev/null +++ b/homeassistant/components/insteon/translations/pl.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "change_hub_config": { + "data": { + "host": "Adres IP", + "password": "Has\u0142o", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json index be84de305ce..4e44844483d 100644 --- a/homeassistant/components/insteon/translations/pt.json +++ b/homeassistant/components/insteon/translations/pt.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "hubv1": { "data": { @@ -15,13 +19,18 @@ "hubv2": { "data": { "host": "Endere\u00e7o IP", - "password": "Senha", + "password": "Palavra-passe", "port": "Porta", - "username": "Utilizador" + "username": "Nome de Utilizador" }, "description": "Configure o Insteon Hub Vers\u00e3o 2.", "title": "Insteon Hub vers\u00e3o 2" }, + "plm": { + "data": { + "device": "Caminho do Dispositivo USB" + } + }, "user": { "data": { "modem_type": "Tipo de modem." @@ -30,5 +39,20 @@ "title": "Insteon" } } + }, + "options": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "change_hub_config": { + "data": { + "host": "Endere\u00e7o IP", + "password": "Palavra-passe", + "port": "Porta", + "username": "Nome de Utilizador" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/pt.json b/homeassistant/components/juicenet/translations/pt.json new file mode 100644 index 00000000000..0c5c7760566 --- /dev/null +++ b/homeassistant/components/juicenet/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/pt.json b/homeassistant/components/kodi/translations/pt.json index 25e47d0be96..12aef9a997d 100644 --- a/homeassistant/components/kodi/translations/pt.json +++ b/homeassistant/components/kodi/translations/pt.json @@ -1,8 +1,40 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { + "credentials": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "host": { + "data": { + "host": "Servidor", + "port": "Porta" + } + }, "user": { + "data": { + "host": "Servidor", + "port": "Porta" + }, "description": "Informa\u00e7\u00f5es de conex\u00e3o Kodi. Certifique-se de habilitar \"Permitir controle do Kodi via HTTP\" em Sistema / Configura\u00e7\u00f5es / Rede / Servi\u00e7os." + }, + "ws_port": { + "data": { + "ws_port": "Porta" + } } } } diff --git a/homeassistant/components/life360/translations/pt.json b/homeassistant/components/life360/translations/pt.json index acca16f95b3..5a6bb471b5e 100644 --- a/homeassistant/components/life360/translations/pt.json +++ b/homeassistant/components/life360/translations/pt.json @@ -5,7 +5,9 @@ }, "error": { "invalid_credentials": "Credenciais inv\u00e1lidas", - "invalid_username": "Nome de utilizador incorreto" + "invalid_username": "Nome de utilizador incorreto", + "unexpected": "Erro inesperado ao comunicar com o servidor do Life360", + "user_already_configured": "Conta j\u00e1 configurada" }, "step": { "user": { diff --git a/homeassistant/components/light/translations/fr.json b/homeassistant/components/light/translations/fr.json index fb6f7f72c9e..c2ca658370b 100644 --- a/homeassistant/components/light/translations/fr.json +++ b/homeassistant/components/light/translations/fr.json @@ -19,7 +19,7 @@ }, "state": { "_": { - "off": "\u00c9teinte", + "off": "Inactif", "on": "Actif" } }, diff --git a/homeassistant/components/logi_circle/translations/fr.json b/homeassistant/components/logi_circle/translations/fr.json index 170edcaf48a..003f44963c9 100644 --- a/homeassistant/components/logi_circle/translations/fr.json +++ b/homeassistant/components/logi_circle/translations/fr.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Suivez le lien ci-dessous et acceptez acc\u00e8s \u00e0 votre compte Logi Circle, puis revenez et appuyez sur Envoyer ci-dessous. \n\n [Lien] ( {authorization_url} )", + "description": "Suivez le lien ci-dessous et **acceptez** acc\u00e8s \u00e0 votre compte Logi Circle, puis revenez et appuyez sur**Envoyer** ci-dessous. \n\n [Lien] ( {authorization_url} )", "title": "Authentifier avec Logi Circle" }, "user": { diff --git a/homeassistant/components/media_player/translations/fr.json b/homeassistant/components/media_player/translations/fr.json index 5ae84d5ba6f..f3992f74616 100644 --- a/homeassistant/components/media_player/translations/fr.json +++ b/homeassistant/components/media_player/translations/fr.json @@ -11,8 +11,8 @@ "state": { "_": { "idle": "En veille", - "off": "\u00c9teint", - "on": "Marche", + "off": "Inactif", + "on": "Actif", "paused": "En pause", "playing": "Lecture en cours", "standby": "En veille" diff --git a/homeassistant/components/melcloud/translations/pt.json b/homeassistant/components/melcloud/translations/pt.json index 767c4da7968..25623dc04a8 100644 --- a/homeassistant/components/melcloud/translations/pt.json +++ b/homeassistant/components/melcloud/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Integra\u00e7\u00e3o com o MELCloud j\u00e1 configurada para este email. O token de acesso foi atualizado." }, + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/monoprice/translations/pt.json b/homeassistant/components/monoprice/translations/pt.json index 0077ceddd46..ccc0fc7c477 100644 --- a/homeassistant/components/monoprice/translations/pt.json +++ b/homeassistant/components/monoprice/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/myq/translations/pt.json b/homeassistant/components/myq/translations/pt.json index b4642359973..4a071063d47 100644 --- a/homeassistant/components/myq/translations/pt.json +++ b/homeassistant/components/myq/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/neato/translations/pt.json b/homeassistant/components/neato/translations/pt.json index b4642359973..f0019df646a 100644 --- a/homeassistant/components/neato/translations/pt.json +++ b/homeassistant/components/neato/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unexpected_error": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index abb1864512b..fd4072dc54a 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -6,7 +6,7 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "create_entry": { - "default": "Authentification r\u00e9ussie avec Netatmo." + "default": "Authentification r\u00e9ussie" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/netatmo/translations/pt.json b/homeassistant/components/netatmo/translations/pt.json index 634d4810ca0..f9199091a85 100644 --- a/homeassistant/components/netatmo/translations/pt.json +++ b/homeassistant/components/netatmo/translations/pt.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "create_entry": { + "default": "Autenticado com sucesso" } }, "options": { diff --git a/homeassistant/components/nexia/translations/pt.json b/homeassistant/components/nexia/translations/pt.json index b4642359973..4a071063d47 100644 --- a/homeassistant/components/nexia/translations/pt.json +++ b/homeassistant/components/nexia/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/pt.json b/homeassistant/components/nightscout/translations/pt.json new file mode 100644 index 00000000000..657ce03e544 --- /dev/null +++ b/homeassistant/components/nightscout/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notify/translations/fr.json b/homeassistant/components/notify/translations/fr.json index bfcad73e8e6..debb3433596 100644 --- a/homeassistant/components/notify/translations/fr.json +++ b/homeassistant/components/notify/translations/fr.json @@ -1,3 +1,3 @@ { - "title": "Notifier" + "title": "Notifications" } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/fr.json b/homeassistant/components/notion/translations/fr.json index c9bb0d6fa62..b5b6005ab9b 100644 --- a/homeassistant/components/notion/translations/fr.json +++ b/homeassistant/components/notion/translations/fr.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Nom d'utilisateur / adresse e-mail" + "username": "Nom d'utilisateur" }, "title": "Veuillez saisir vos informations" } diff --git a/homeassistant/components/nuheat/translations/pt.json b/homeassistant/components/nuheat/translations/pt.json index b4642359973..4a071063d47 100644 --- a/homeassistant/components/nuheat/translations/pt.json +++ b/homeassistant/components/nuheat/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nut/translations/pt.json b/homeassistant/components/nut/translations/pt.json index 4ebc88bb1cf..5edf0b18dd1 100644 --- a/homeassistant/components/nut/translations/pt.json +++ b/homeassistant/components/nut/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "ups": { "data": { diff --git a/homeassistant/components/nws/translations/fr.json b/homeassistant/components/nws/translations/fr.json index 8b1b01ec74b..86db25ebd11 100644 --- a/homeassistant/components/nws/translations/fr.json +++ b/homeassistant/components/nws/translations/fr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Cl\u00e9 API (e-mail)", + "api_key": "Cl\u00e9 d'API", "latitude": "Latitude", "longitude": "Longitude", "station": "Code de la station METAR" diff --git a/homeassistant/components/nzbget/translations/fr.json b/homeassistant/components/nzbget/translations/fr.json new file mode 100644 index 00000000000..e77d73c8d38 --- /dev/null +++ b/homeassistant/components/nzbget/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ssl": "NZBGet utilise un certificat SSL", + "verify_ssl": "NZBGet utilise un certificat appropri\u00e9" + }, + "title": "Se connecter \u00e0 NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour (en secondes)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/pt.json b/homeassistant/components/nzbget/translations/pt.json index 93aa814f47a..2fa9743db44 100644 --- a/homeassistant/components/nzbget/translations/pt.json +++ b/homeassistant/components/nzbget/translations/pt.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "J\u00e1 se encontra configurado. S\u00f3 \u00e9 permitida uma configura\u00e7\u00e3o.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "unknown": "Erro inesperado" }, "error": { - "cannot_connect": "Erro ao conectar-se", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "flow_title": "NZBGet: {name}", @@ -14,10 +14,10 @@ "data": { "host": "Servidor", "name": "Nome", - "password": "Senha", + "password": "Palavra-passe", "port": "Porta", "ssl": "NZBGet usa um certificado SSL", - "username": "Utilizador", + "username": "Nome de Utilizador", "verify_ssl": "NZBGet usa um certificado adequado" }, "title": "Conectar ao NZBGet" diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json index 546b76a1802..2a1ce2d8d73 100644 --- a/homeassistant/components/ovo_energy/translations/fr.json +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -2,6 +2,11 @@ "config": { "error": { "connection_error": "\u00c9chec de connexion" + }, + "step": { + "user": { + "title": "Ajouter un compte OVO Energy" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/pt.json b/homeassistant/components/ovo_energy/translations/pt.json new file mode 100644 index 00000000000..c49725f2fd4 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "already_configured": "Conta j\u00e1 configurada", + "connection_error": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/pt.json b/homeassistant/components/plugwise/translations/pt.json new file mode 100644 index 00000000000..0c5c7760566 --- /dev/null +++ b/homeassistant/components/plugwise/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index 75f7a252102..9fe22410188 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -5,14 +5,14 @@ "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "external_setup": "Point correctement configur\u00e9 \u00e0 partir d\u2019un autre flux.", - "no_flows": "Vous devez configurer Point avant de pouvoir vous authentifier avec celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/point/)." + "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "create_entry": { - "default": "Authentification r\u00e9ussie avec Minut pour votre (vos) p\u00e9riph\u00e9rique (s) Point" + "default": "Authentification r\u00e9ussie" }, "error": { "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", - "no_token": "Non authentifi\u00e9 avec Minut" + "no_token": "Jeton d'acc\u00e8s non valide" }, "step": { "auth": { @@ -23,8 +23,8 @@ "data": { "flow_impl": "Fournisseur" }, - "description": "Choisissez via quel fournisseur d'authentification vous souhaitez vous authentifier avec Point.", - "title": "Fournisseur d'authentification" + "description": "Voulez-vous commencer la configuration?", + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } } } diff --git a/homeassistant/components/poolsense/translations/pt.json b/homeassistant/components/poolsense/translations/pt.json index db5e24356b7..0dc8303b485 100644 --- a/homeassistant/components/poolsense/translations/pt.json +++ b/homeassistant/components/poolsense/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, diff --git a/homeassistant/components/powerwall/translations/pt.json b/homeassistant/components/powerwall/translations/pt.json new file mode 100644 index 00000000000..0c5c7760566 --- /dev/null +++ b/homeassistant/components/powerwall/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/en.json b/homeassistant/components/progettihwsw/translations/en.json index 7ec64794c91..68ef7389a26 100644 --- a/homeassistant/components/progettihwsw/translations/en.json +++ b/homeassistant/components/progettihwsw/translations/en.json @@ -1,11 +1,12 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, "error": { "cannot_connect": "Failed to connect", - "unknown": "Unknown error" - }, - "abort": { - "already_configured": "This board has already been set up." + "unknown": "Unexpected error", + "wrong_info_relay_modes": "Relay mode selection must be monostable or bistable." }, "step": { "relay_modes": { diff --git a/homeassistant/components/progettihwsw/translations/es.json b/homeassistant/components/progettihwsw/translations/es.json index 3da2bc22c45..a4fa294761d 100644 --- a/homeassistant/components/progettihwsw/translations/es.json +++ b/homeassistant/components/progettihwsw/translations/es.json @@ -35,5 +35,6 @@ "title": "Configurar tablero" } } - } + }, + "title": "Automatizaci\u00f3n ProgettiHWSW" } \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/fr.json b/homeassistant/components/progettihwsw/translations/fr.json new file mode 100644 index 00000000000..5a8cc6c8bf6 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/fr.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "relay_modes": { + "data": { + "relay_1": "Relais 1", + "relay_10": "Relais 10", + "relay_11": "Relais 11", + "relay_12": "Relais 12", + "relay_13": "Relais 13", + "relay_14": "Relais 14", + "relay_15": "Relais 15", + "relay_16": "Relais 16", + "relay_2": "Relais 2", + "relay_3": "Relais 3", + "relay_4": "Relais 4", + "relay_5": "Relais 5", + "relay_6": "Relais 6", + "relay_7": "Relais 7", + "relay_8": "Relais 8" + }, + "title": "Configurer les relais" + }, + "user": { + "data": { + "port": "Port" + }, + "title": "Configurer le tableau" + } + } + }, + "title": "Automatisation ProgettiHWSW" +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/pt.json b/homeassistant/components/progettihwsw/translations/pt.json index 5fac50b2cc1..89405d0262b 100644 --- a/homeassistant/components/progettihwsw/translations/pt.json +++ b/homeassistant/components/progettihwsw/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Erro ao conectar-se", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado", "wrong_info_relay_modes": "A sele\u00e7\u00e3o do modo de rel\u00e9 deve ser monoest\u00e1vel ou biest\u00e1vel." }, diff --git a/homeassistant/components/progettihwsw/translations/ru.json b/homeassistant/components/progettihwsw/translations/ru.json index 11ecb8bfc33..a136e30015d 100644 --- a/homeassistant/components/progettihwsw/translations/ru.json +++ b/homeassistant/components/progettihwsw/translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", diff --git a/homeassistant/components/rachio/translations/pt.json b/homeassistant/components/rachio/translations/pt.json new file mode 100644 index 00000000000..4c01137c496 --- /dev/null +++ b/homeassistant/components/rachio/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave de API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/fr.json b/homeassistant/components/remote/translations/fr.json index 632bc1044d4..ad2461e1f0a 100644 --- a/homeassistant/components/remote/translations/fr.json +++ b/homeassistant/components/remote/translations/fr.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Arr\u00eat", + "off": "Inactif", "on": "Actif" } }, diff --git a/homeassistant/components/rfxtrx/translations/pt.json b/homeassistant/components/rfxtrx/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/pt.json b/homeassistant/components/ring/translations/pt.json index b4642359973..4a071063d47 100644 --- a/homeassistant/components/ring/translations/pt.json +++ b/homeassistant/components/ring/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json index a0968f4aa50..58518e7b7c6 100644 --- a/homeassistant/components/risco/translations/es.json +++ b/homeassistant/components/risco/translations/es.json @@ -20,6 +20,16 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "Armada Ausente", + "armed_custom_bypass": "Armada Personalizada", + "armed_home": "Armada en Casa", + "armed_night": "Armada Noche" + }, + "description": "Selecciona en qu\u00e9 estado quieres configurar la alarma Risco cuando armes la alarma de Home Assistant", + "title": "Mapear estados de Home Assistant a estados Risco" + }, "init": { "data": { "code_arm_required": "Requiere un c\u00f3digo PIN para armar", @@ -33,8 +43,11 @@ "A": "Grupo A", "B": "Grupo B", "C": "Grupo C", - "D": "Grupo D" + "D": "Grupo D", + "arm": "Armada (AUSENTE)", + "partial_arm": "Parcialmente Armada (EN CASA)" }, + "description": "Selecciona qu\u00e9 estado reportar\u00e1 la alarma de tu Home Assistant para cada estado reportado por Risco", "title": "Asignar estados de Risco a estados de Home Assistant" } } diff --git a/homeassistant/components/risco/translations/pt.json b/homeassistant/components/risco/translations/pt.json index 02f17e897ec..eb4bf7ba6a7 100644 --- a/homeassistant/components/risco/translations/pt.json +++ b/homeassistant/components/risco/translations/pt.json @@ -1,4 +1,22 @@ { + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + }, "options": { "step": { "ha_to_risco": { diff --git a/homeassistant/components/roku/translations/pt.json b/homeassistant/components/roku/translations/pt.json index ce7cbc3f548..7880adf5fff 100644 --- a/homeassistant/components/roku/translations/pt.json +++ b/homeassistant/components/roku/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/roon/translations/pt.json b/homeassistant/components/roon/translations/pt.json new file mode 100644 index 00000000000..6e36ff769cd --- /dev/null +++ b/homeassistant/components/roon/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/pt.json b/homeassistant/components/sense/translations/pt.json index 2933743c867..196be985b6a 100644 --- a/homeassistant/components/sense/translations/pt.json +++ b/homeassistant/components/sense/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sentry/translations/pt.json b/homeassistant/components/sentry/translations/pt.json new file mode 100644 index 00000000000..65db7f4018f --- /dev/null +++ b/homeassistant/components/sentry/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/en.json b/homeassistant/components/sharkiq/translations/en.json index 395c8b68e66..d305eaf07e8 100644 --- a/homeassistant/components/sharkiq/translations/en.json +++ b/homeassistant/components/sharkiq/translations/en.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured_account": "Account is already configured", - "reauth_successful": "Reauthentication successful" + "cannot_connect": "Failed to connect", + "reauth_successful": "Access Token updated successfully", + "unknown": "Unexpected error" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/sharkiq/translations/es.json b/homeassistant/components/sharkiq/translations/es.json index 70179f4a8e1..2d5ff46d50c 100644 --- a/homeassistant/components/sharkiq/translations/es.json +++ b/homeassistant/components/sharkiq/translations/es.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured_account": "La cuenta ya ha sido configurada" + "already_configured_account": "La cuenta ya ha sido configurada", + "cannot_connect": "No se pudo conectar", + "reauth_successful": "Token de acceso actualizado correctamente", + "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", @@ -9,6 +12,12 @@ "unknown": "Error inesperado" }, "step": { + "reauth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/sharkiq/translations/pt.json b/homeassistant/components/sharkiq/translations/pt.json index 3469901540c..973b6681d31 100644 --- a/homeassistant/components/sharkiq/translations/pt.json +++ b/homeassistant/components/sharkiq/translations/pt.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured_account": "Conta j\u00e1 se encontra configurada" + "already_configured_account": "Conta j\u00e1 configurada" }, "error": { - "cannot_connect": "Erro ao conectar-se", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "password": "Senha", - "username": "Utilizador" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/sharkiq/translations/ru.json b/homeassistant/components/sharkiq/translations/ru.json index a2ac3f08f36..3bbdbc5ac58 100644 --- a/homeassistant/components/sharkiq/translations/ru.json +++ b/homeassistant/components/sharkiq/translations/ru.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "reauth_successful": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +12,12 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index 5c3e86e73ad..546007af0d1 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "auth_not_supported": "Shelly devices requiring authentication are not currently supported.", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" }, "flow_title": "Shelly: {name}", "step": { @@ -15,13 +16,13 @@ }, "credentials": { "data": { - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" + "password": "Password", + "username": "Username" } }, "user": { "data": { - "host": "[%key:common::config_flow::data::host%]" + "host": "Host" } } } diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index fa687f586d3..153c9de33da 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -6,6 +6,7 @@ "error": { "auth_not_supported": "Los dispositivos Shelly que requieren autenticaci\u00f3n no son compatibles actualmente.", "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "flow_title": "Shelly: {name}", @@ -13,6 +14,12 @@ "confirm_discovery": { "description": "\u00bfQuieres configurar el {model} en {host}?" }, + "credentials": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/shelly/translations/pt.json b/homeassistant/components/shelly/translations/pt.json index 30ebb24a047..2252c7c1eb9 100644 --- a/homeassistant/components/shelly/translations/pt.json +++ b/homeassistant/components/shelly/translations/pt.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 se encontra configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "auth_not_supported": "Dispositivos Shelly que requerem autentica\u00e7\u00e3o n\u00e3o s\u00e3o atualmente suportados.", - "cannot_connect": "Erro ao conectar-se", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "flow_title": "Shelly: {name}", diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index f265a49b6d4..570e6f8d7c7 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -6,6 +6,7 @@ "error": { "auth_not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Shelly, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0438\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "Shelly: {name}", @@ -13,6 +14,12 @@ "confirm_discovery": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c {model} ({host}) ?" }, + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/simplisafe/translations/pt.json b/homeassistant/components/simplisafe/translations/pt.json index ab54288d91a..9c98515448b 100644 --- a/homeassistant/components/simplisafe/translations/pt.json +++ b/homeassistant/components/simplisafe/translations/pt.json @@ -2,9 +2,15 @@ "config": { "error": { "identifier_exists": "Conta j\u00e1 registada", - "invalid_credentials": "Credenciais inv\u00e1lidas" + "invalid_credentials": "Credenciais inv\u00e1lidas", + "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/smappee/translations/pt.json b/homeassistant/components/smappee/translations/pt.json new file mode 100644 index 00000000000..aba871acf6b --- /dev/null +++ b/homeassistant/components/smappee/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "local": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/pt.json b/homeassistant/components/smart_meter_texas/translations/pt.json new file mode 100644 index 00000000000..7953cf5625c --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/fr.json b/homeassistant/components/solaredge/translations/fr.json index b03a29ad2e1..f8aec1fa230 100644 --- a/homeassistant/components/solaredge/translations/fr.json +++ b/homeassistant/components/solaredge/translations/fr.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "La cl\u00e9 API pour ce site", + "api_key": "Cl\u00e9 d'API", "name": "Le nom de cette installation", "site_id": "L'identifiant de site SolarEdge" }, diff --git a/homeassistant/components/solaredge/translations/pt.json b/homeassistant/components/solaredge/translations/pt.json new file mode 100644 index 00000000000..01078bbddfe --- /dev/null +++ b/homeassistant/components/solaredge/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave de API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/fr.json b/homeassistant/components/solarlog/translations/fr.json index 2de03d82c31..3e950af8564 100644 --- a/homeassistant/components/solarlog/translations/fr.json +++ b/homeassistant/components/solarlog/translations/fr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Le nom d'h\u00f4te ou l'adresse IP de votre p\u00e9riph\u00e9rique Solar-Log", + "host": "H\u00f4te", "name": "Le pr\u00e9fixe \u00e0 utiliser pour vos capteurs Solar-Log" }, "title": "D\u00e9finissez votre connexion Solar-Log" diff --git a/homeassistant/components/solarlog/translations/pt.json b/homeassistant/components/solarlog/translations/pt.json index ce7cbc3f548..88cfc4a797f 100644 --- a/homeassistant/components/solarlog/translations/pt.json +++ b/homeassistant/components/solarlog/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o, por favor verifique o endere\u00e7o do servidor" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/spider/translations/pt.json b/homeassistant/components/spider/translations/pt.json new file mode 100644 index 00000000000..0ef1127e19d --- /dev/null +++ b/homeassistant/components/spider/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index 71020022ef1..95c29f7a336 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "S\u00f3lo puedes configurar una cuenta de Spotify.", "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." + "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.", + "reauth_account_mismatch": "La cuenta de Spotify con la que est\u00e1s autenticado, no coincide con la cuenta necesaria para re-autenticaci\u00f3n." }, "create_entry": { "default": "Autentificado con \u00e9xito con Spotify." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Spotify necesita volver a autenticarse con Spotify para la cuenta: {account}", + "title": "Volver a autenticar con Spotify" } } } diff --git a/homeassistant/components/squeezebox/translations/pt.json b/homeassistant/components/squeezebox/translations/pt.json index e3e9813142b..97ced548ed5 100644 --- a/homeassistant/components/squeezebox/translations/pt.json +++ b/homeassistant/components/squeezebox/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", diff --git a/homeassistant/components/switch/translations/fr.json b/homeassistant/components/switch/translations/fr.json index d0349369515..997986924a3 100644 --- a/homeassistant/components/switch/translations/fr.json +++ b/homeassistant/components/switch/translations/fr.json @@ -17,7 +17,7 @@ "state": { "_": { "off": "Inactif", - "on": "On" + "on": "Actif" } }, "title": "Interrupteur" diff --git a/homeassistant/components/syncthru/translations/pt.json b/homeassistant/components/syncthru/translations/pt.json index 03ddb9523ec..6463b4241c6 100644 --- a/homeassistant/components/syncthru/translations/pt.json +++ b/homeassistant/components/syncthru/translations/pt.json @@ -1,17 +1,22 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "unknown_state": "Estado da impressora desconhecido, verifique a conectividade de URL e de rede" }, "step": { "confirm": { "data": { - "name": "Nome" + "name": "Nome", + "url": "URL da interface Web" } }, "user": { "data": { - "name": "Nome" + "name": "Nome", + "url": "URL da interface Web" } } } diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index 74604a96c11..6c8b627a76b 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -21,7 +21,7 @@ "link": { "data": { "password": "Mot de passe", - "port": "Port (facultatif)", + "port": "Port", "ssl": "Utilisez SSL/TLS pour vous connecter \u00e0 votre NAS", "username": "Nom d'utilisateur" }, @@ -32,7 +32,7 @@ "data": { "host": "Nom d'h\u00f4te ou adresse IP", "password": "Mot de passe", - "port": "Port (facultatif)", + "port": "Port", "ssl": "Utilisez SSL/TLS pour vous connecter \u00e0 votre NAS", "username": "Nom d'utilisateur" }, diff --git a/homeassistant/components/tado/translations/pt.json b/homeassistant/components/tado/translations/pt.json index b4642359973..4a071063d47 100644 --- a/homeassistant/components/tado/translations/pt.json +++ b/homeassistant/components/tado/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tesla/translations/fr.json b/homeassistant/components/tesla/translations/fr.json index 638557f1035..96fe2e9d082 100644 --- a/homeassistant/components/tesla/translations/fr.json +++ b/homeassistant/components/tesla/translations/fr.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Adresse e-mail" + "username": "Email" }, "description": "Veuillez saisir vos informations.", "title": "Tesla - Configuration" diff --git a/homeassistant/components/timer/translations/fr.json b/homeassistant/components/timer/translations/fr.json index 7c15fdc8dd6..6f9194f9bbf 100644 --- a/homeassistant/components/timer/translations/fr.json +++ b/homeassistant/components/timer/translations/fr.json @@ -1,9 +1,9 @@ { "state": { "_": { - "active": "actif", - "idle": "en veille", - "paused": "en pause" + "active": "Actif", + "idle": "En veille", + "paused": "En pause" } } } \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/pt.json b/homeassistant/components/twentemilieu/translations/pt.json new file mode 100644 index 00000000000..3d982a98998 --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "connection_error": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/pt.json b/homeassistant/components/upb/translations/pt.json new file mode 100644 index 00000000000..0c5c7760566 --- /dev/null +++ b/homeassistant/components/upb/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/fr.json b/homeassistant/components/vesync/translations/fr.json index 6db0922e684..0d00100ae05 100644 --- a/homeassistant/components/vesync/translations/fr.json +++ b/homeassistant/components/vesync/translations/fr.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Mot de passe", - "username": "Adresse e-mail" + "username": "Email" }, "title": "Entrez vos identifiants" } diff --git a/homeassistant/components/vizio/translations/pt.json b/homeassistant/components/vizio/translations/pt.json index 695ad541026..b1a4f0d7b36 100644 --- a/homeassistant/components/vizio/translations/pt.json +++ b/homeassistant/components/vizio/translations/pt.json @@ -8,6 +8,8 @@ }, "user": { "data": { + "access_token": "Token de Acesso", + "host": "Servidor", "name": "Nome" } } diff --git a/homeassistant/components/volumio/translations/pt.json b/homeassistant/components/volumio/translations/pt.json index f681da4210f..3616141b35b 100644 --- a/homeassistant/components/volumio/translations/pt.json +++ b/homeassistant/components/volumio/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wilight/translations/pt.json b/homeassistant/components/wilight/translations/pt.json index 51aad9406f0..5b1005a95f5 100644 --- a/homeassistant/components/wilight/translations/pt.json +++ b/homeassistant/components/wilight/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 se encontra configurado", + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "not_supported_device": "Este WiLight n\u00e3o \u00e9 compat\u00edvel atualmente", "not_wilight_device": "Este dispositivo n\u00e3o \u00e9 WiLight" }, diff --git a/homeassistant/components/wolflink/translations/pt.json b/homeassistant/components/wolflink/translations/pt.json new file mode 100644 index 00000000000..7953cf5625c --- /dev/null +++ b/homeassistant/components/wolflink/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/pt.json b/homeassistant/components/xiaomi_aqara/translations/pt.json index 98d4f8ff63f..1983f8b28a5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt.json @@ -1,11 +1,19 @@ { "config": { + "error": { + "invalid_host": "Endere\u00e7o IP Inv\u00e1lido" + }, "step": { "settings": { "data": { "name": "Nome da Gateway" }, "description": "A chave (palavra-passe) pode ser recuperada usando este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se a chave n\u00e3o for fornecida, apenas os sensores estar\u00e3o acess\u00edveis" + }, + "user": { + "data": { + "host": "Endere\u00e7o IP (optional)" + } } } } diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json new file mode 100644 index 00000000000..08c2ca92dea --- /dev/null +++ b/homeassistant/components/yeelight/translations/es.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP" + }, + "description": "Si dejas la direcci\u00f3n IP vac\u00eda, se usar\u00e1 descubrimiento para encontrar dispositivos." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Modelo (opcional)", + "nightlight_switch": "Usar interruptor de luz nocturna", + "save_on_change": "Guardar estado al cambiar", + "transition": "Tiempo de transici\u00f3n (ms)", + "use_music_mode": "Activar el Modo M\u00fasica" + }, + "description": "Si dejas el modelo vac\u00edo, se detectar\u00e1 autom\u00e1ticamente." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/fr.json b/homeassistant/components/yeelight/translations/fr.json new file mode 100644 index 00000000000..cd7a76ca4df --- /dev/null +++ b/homeassistant/components/yeelight/translations/fr.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "pick_device": { + "data": { + "device": "Appareil" + } + }, + "user": { + "data": { + "ip_address": "Adresse IP" + }, + "description": "Si vous laissez l'adresse IP vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Mod\u00e8le (facultatif)", + "nightlight_switch": "Utiliser l'interrupteur de la veilleuse", + "save_on_change": "Sauvegarder le statut lors d'un changement", + "transition": "Temps de transition (ms)", + "use_music_mode": "Activer le mode musique" + }, + "description": "Si vous ne pr\u00e9cisez pas le mod\u00e8le, il sera automatiquement d\u00e9tect\u00e9." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/no.json b/homeassistant/components/yeelight/translations/no.json new file mode 100644 index 00000000000..07f0b7be05c --- /dev/null +++ b/homeassistant/components/yeelight/translations/no.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes." + }, + "step": { + "pick_device": { + "data": { + "device": "Enhet" + } + }, + "user": { + "data": { + "ip_address": "IP adresse" + }, + "description": "Hvis du lar IP-adressen st\u00e5 tom, brukes oppdagelsen til \u00e5 finne enheter." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Modell (valgfritt)", + "nightlight_switch": "Bruk nattlysbryter", + "save_on_change": "Lagre status ved endring", + "transition": "Overgangstid (ms)", + "use_music_mode": "Aktiver musikkmodus" + }, + "description": "Hvis du lar modellen v\u00e6re tom, blir den automatisk oppdaget." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/pt.json b/homeassistant/components/yeelight/translations/pt.json new file mode 100644 index 00000000000..7da4aff16ec --- /dev/null +++ b/homeassistant/components/yeelight/translations/pt.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + }, + "description": "Se voc\u00ea deixar o modelo vazio, ele ser\u00e1 detectado automaticamente." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Modelo (Opcional)", + "nightlight_switch": "Use o bot\u00e3o Nightlight", + "save_on_change": "Salvar status ao alterar", + "transition": "Tempo de transi\u00e7\u00e3o (ms)", + "use_music_mode": "Ativar modo de m\u00fasica" + }, + "description": "Se voc\u00ea deixar o modelo vazio, ele ser\u00e1 detectado automaticamente." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file From e921f72d31939c25505d235e9d330359db7f874a Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 1 Sep 2020 20:40:45 -0500 Subject: [PATCH 556/862] Use media state to better represent roku state (#39540) * use media state to better represent roku state * Update media_player.py * Update media_player.py * Update media_player.py * Update media_player.py * Update media_player.py * Update media_player.py * Update media_player.py * Update media_player.py * Update test_media_player.py * Update test_media_player.py * Update test_media_player.py * Update media_player.py * Update test_media_player.py * Update test_media_player.py --- homeassistant/components/roku/media_player.py | 40 +++++++++++++++++-- tests/components/roku/test_media_player.py | 26 +++++++++++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index d4a5f656c82..96d23872dfa 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -22,6 +22,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import ( STATE_HOME, STATE_IDLE, + STATE_ON, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, @@ -77,6 +78,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): self._unique_id = unique_id + def _media_playback_trackable(self) -> bool: + """Detect if we have enough media data to track playback.""" + if self.coordinator.data.media is None or self.coordinator.data.media.live: + return False + + return self.coordinator.data.media.duration > 0 + @property def unique_id(self) -> str: """Return the unique ID for this entity.""" @@ -100,11 +108,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if self.coordinator.data.app.name == "Roku": return STATE_HOME - if self.coordinator.data.media and self.coordinator.data.media.paused: - return STATE_PAUSED + if self.coordinator.data.media: + if self.coordinator.data.media.paused: + return STATE_PAUSED + return STATE_PLAYING if self.coordinator.data.app.name: - return STATE_PLAYING + return STATE_ON return None @@ -170,6 +180,30 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return None + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + if self._media_playback_trackable(): + return self.coordinator.data.media.duration + + return None + + @property + def media_position(self): + """Position of current playing media in seconds.""" + if self._media_playback_trackable(): + return self.coordinator.data.media.position + + return None + + @property + def media_position_updated_at(self): + """When was the position of the current playing media valid.""" + if self._media_playback_trackable(): + return self.coordinator.data.media.at + + return None + @property def source(self) -> str: """Return the current input source.""" diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index a05cde5a596..4df0c95ad07 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -10,6 +10,8 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_DURATION, + ATTR_MEDIA_POSITION, ATTR_MEDIA_TITLE, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MP_DOMAIN, @@ -43,6 +45,7 @@ from homeassistant.const import ( SERVICE_VOLUME_UP, STATE_HOME, STATE_IDLE, + STATE_ON, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, @@ -207,7 +210,7 @@ async def test_attributes_app( await setup_integration(hass, aioclient_mock, app="netflix") state = hass.states.get(MAIN_ENTITY_ID) - assert state.state == STATE_PLAYING + assert state.state == STATE_ON assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP assert state.attributes.get(ATTR_APP_ID) == "12" @@ -215,6 +218,23 @@ async def test_attributes_app( assert state.attributes.get(ATTR_INPUT_SOURCE) == "Netflix" +async def test_attributes_app_media_playing( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test attributes for app with playing media.""" + await setup_integration(hass, aioclient_mock, app="pluto", media_state="play") + + state = hass.states.get(MAIN_ENTITY_ID) + assert state.state == STATE_PLAYING + + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP + assert state.attributes.get(ATTR_MEDIA_DURATION) == 6496 + assert state.attributes.get(ATTR_MEDIA_POSITION) == 38 + assert state.attributes.get(ATTR_APP_ID) == "74519" + assert state.attributes.get(ATTR_APP_NAME) == "Pluto TV - It's Free TV" + assert state.attributes.get(ATTR_INPUT_SOURCE) == "Pluto TV - It's Free TV" + + async def test_attributes_app_media_paused( hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: @@ -225,6 +245,8 @@ async def test_attributes_app_media_paused( assert state.state == STATE_PAUSED assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP + assert state.attributes.get(ATTR_MEDIA_DURATION) == 6496 + assert state.attributes.get(ATTR_MEDIA_POSITION) == 313 assert state.attributes.get(ATTR_APP_ID) == "74519" assert state.attributes.get(ATTR_APP_NAME) == "Pluto TV - It's Free TV" assert state.attributes.get(ATTR_INPUT_SOURCE) == "Pluto TV - It's Free TV" @@ -259,7 +281,7 @@ async def test_tv_attributes( ) state = hass.states.get(TV_ENTITY_ID) - assert state.state == STATE_PLAYING + assert state.state == STATE_ON assert state.attributes.get(ATTR_APP_ID) == "tvinput.dtv" assert state.attributes.get(ATTR_APP_NAME) == "Antenna TV" From acfb4e462ebba583ebbe957674c41bd661e65c23 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Wed, 2 Sep 2020 06:34:00 +0100 Subject: [PATCH 557/862] Bump openwrt-luci-rpc to 1.1.6 (#39561) * Update requirements_all.txt * Update manifest.json --- homeassistant/components/luci/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 5699972a053..d18cbae5103 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -2,6 +2,6 @@ "domain": "luci", "name": "OpenWRT (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", - "requirements": ["openwrt-luci-rpc==1.1.5"], + "requirements": ["openwrt-luci-rpc==1.1.6"], "codeowners": ["@fbradyirl", "@mzdrale"] } diff --git a/requirements_all.txt b/requirements_all.txt index ec93f932b1c..64287ff4d7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ opensensemap-api==0.1.5 openwebifpy==3.1.1 # homeassistant.components.luci -openwrt-luci-rpc==1.1.5 +openwrt-luci-rpc==1.1.6 # homeassistant.components.oru oru==0.1.11 From 07f2f78b0265cbc35bffe324d8d533adf7cecb9d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 2 Sep 2020 08:56:27 +0200 Subject: [PATCH 558/862] Add shelly overtemp and vibration sensors (#39556) --- homeassistant/components/shelly/binary_sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 236b2a1cb5e..52a752a64f9 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_VIBRATION, BinarySensorEntity, ) @@ -15,7 +16,9 @@ SENSORS = { "dwIsOpened": DEVICE_CLASS_OPENING, "flood": DEVICE_CLASS_MOISTURE, "overpower": None, + "overtemp": None, "smoke": DEVICE_CLASS_SMOKE, + "vibration": DEVICE_CLASS_VIBRATION, } From 225becc89abf76606ffe860e2cd15bb08d9d4b1a Mon Sep 17 00:00:00 2001 From: Paolo Antinori Date: Wed, 2 Sep 2020 09:56:11 +0200 Subject: [PATCH 559/862] Add alexa unofficial specific API support for Italian (#39475) Co-authored-by: ochlocracy <5885236+ochlocracy@users.noreply.github.com> --- .../components/alexa/capabilities.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 032aca32e02..63d8bb751bc 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -866,7 +866,7 @@ class AlexaContactSensor(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-contactsensor.html """ - supported_locales = {"en-CA", "en-US"} + supported_locales = {"en-CA", "en-US", "it-IT"} def __init__(self, hass, entity): """Initialize the entity.""" @@ -905,7 +905,7 @@ class AlexaMotionSensor(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html """ - supported_locales = {"en-CA", "en-US"} + supported_locales = {"en-CA", "en-US", "it-IT"} def __init__(self, hass, entity): """Initialize the entity.""" @@ -1113,7 +1113,22 @@ class AlexaSecurityPanelController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html """ - supported_locales = {"en-AU", "en-CA", "en-IN", "en-US"} + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "es-US", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + "pt_BR", + } def __init__(self, hass, entity): """Initialize the entity.""" From 557684c3ceb754b60478ca65a2240f1f7c8d0336 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2020 03:12:56 -0500 Subject: [PATCH 560/862] Add ability to disable the sqlite3 quick_check (#39479) --- homeassistant/components/recorder/__init__.py | 14 +++- homeassistant/components/recorder/const.py | 2 + homeassistant/components/recorder/util.py | 24 ++++-- tests/components/recorder/test_init.py | 1 + tests/components/recorder/test_util.py | 79 ++++++++++++++++--- 5 files changed, 99 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f93f498987f..9ce950fedfe 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from . import migration, purge -from .const import DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX +from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX from .models import Base, Events, RecorderRuns, States from .util import session_scope, validate_or_move_away_sqlite_database @@ -55,6 +55,7 @@ SERVICE_PURGE_SCHEMA = vol.Schema( DEFAULT_URL = "sqlite:///{hass_config_path}" DEFAULT_DB_FILE = "home-assistant_v2.db" +DEFAULT_DB_INTEGRITY_CHECK = True DEFAULT_DB_MAX_RETRIES = 10 DEFAULT_DB_RETRY_WAIT = 3 KEEPALIVE_TIME = 30 @@ -99,6 +100,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional( CONF_DB_RETRY_WAIT, default=DEFAULT_DB_RETRY_WAIT ): cv.positive_int, + vol.Optional( + CONF_DB_INTEGRITY_CHECK, default=DEFAULT_DB_INTEGRITY_CHECK + ): cv.boolean, } ), ) @@ -156,6 +160,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: commit_interval = conf[CONF_COMMIT_INTERVAL] db_max_retries = conf[CONF_DB_MAX_RETRIES] db_retry_wait = conf[CONF_DB_RETRY_WAIT] + db_integrity_check = conf[CONF_DB_INTEGRITY_CHECK] db_url = conf.get(CONF_DB_URL) if not db_url: @@ -172,6 +177,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: db_retry_wait=db_retry_wait, entity_filter=entity_filter, exclude_t=exclude_t, + db_integrity_check=db_integrity_check, ) instance.async_initialize() instance.start() @@ -204,6 +210,7 @@ class Recorder(threading.Thread): db_retry_wait: int, entity_filter: Callable[[str], bool], exclude_t: List[str], + db_integrity_check: bool, ) -> None: """Initialize the recorder.""" threading.Thread.__init__(self, name="Recorder") @@ -217,6 +224,7 @@ class Recorder(threading.Thread): self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait + self.db_integrity_check = db_integrity_check self.async_db_ready = asyncio.Future() self.engine: Any = None self.run_info: Any = None @@ -547,7 +555,9 @@ class Recorder(threading.Thread): # On systems with very large databases and # very slow disk or cpus, this can take a while. # - validate_or_move_away_sqlite_database(self.db_url) + validate_or_move_away_sqlite_database( + self.db_url, self.db_integrity_check + ) if self.engine is not None: self.engine.dispose() diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index b2ffc91fdb4..a2b5ffc6f2a 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -3,3 +3,5 @@ DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" DOMAIN = "recorder" + +CONF_DB_INTEGRITY_CHECK = "db_integrity_check" diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 7503d0fe774..ed7f5affc56 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -9,7 +9,7 @@ from sqlalchemy.exc import OperationalError, SQLAlchemyError import homeassistant.util.dt as dt_util -from .const import DATA_INSTANCE, SQLITE_URL_PREFIX +from .const import CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, SQLITE_URL_PREFIX from .models import ALL_TABLES, process_timestamp _LOGGER = logging.getLogger(__name__) @@ -21,7 +21,7 @@ SQLITE3_POSTFIXES = ["", "-wal", "-shm"] # This is the maximum time after the recorder ends the session # before we no longer consider startup to be a "restart" and we # should do a check on the sqlite3 database. -MAX_RESTART_TIME = timedelta(minutes=6) +MAX_RESTART_TIME = timedelta(minutes=10) @contextmanager @@ -110,7 +110,7 @@ def execute(qry, to_native=False, validate_entity_ids=True): time.sleep(QUERY_RETRY_WAIT) -def validate_or_move_away_sqlite_database(dburl: str) -> bool: +def validate_or_move_away_sqlite_database(dburl: str, db_integrity_check: bool) -> bool: """Ensure that the database is valid or move it away.""" dbpath = dburl[len(SQLITE_URL_PREFIX) :] @@ -118,7 +118,7 @@ def validate_or_move_away_sqlite_database(dburl: str) -> bool: # Database does not exist yet, this is OK return True - if not validate_sqlite_database(dbpath): + if not validate_sqlite_database(dbpath, db_integrity_check): _move_away_broken_database(dbpath) return False @@ -154,13 +154,13 @@ def basic_sanity_check(cursor): return True -def validate_sqlite_database(dbpath: str) -> bool: +def validate_sqlite_database(dbpath: str, db_integrity_check: bool) -> bool: """Run a quick check on an sqlite database to see if it is corrupt.""" import sqlite3 # pylint: disable=import-outside-toplevel try: conn = sqlite3.connect(dbpath) - run_checks_on_open_db(dbpath, conn.cursor()) + run_checks_on_open_db(dbpath, conn.cursor(), db_integrity_check) conn.close() except sqlite3.DatabaseError: _LOGGER.exception("The database at %s is corrupt or malformed.", dbpath) @@ -169,7 +169,7 @@ def validate_sqlite_database(dbpath: str) -> bool: return True -def run_checks_on_open_db(dbpath, cursor): +def run_checks_on_open_db(dbpath, cursor, db_integrity_check): """Run checks that will generate a sqlite3 exception if there is corruption.""" if basic_sanity_check(cursor) and last_run_was_recently_clean(cursor): _LOGGER.debug( @@ -177,6 +177,16 @@ def run_checks_on_open_db(dbpath, cursor): ) return + if not db_integrity_check: + # Always warn so when it does fail they remember it has + # been manually disabled + _LOGGER.warning( + "The quick_check on the sqlite3 database at %s was skipped because %s was disabled", + dbpath, + CONF_DB_INTEGRITY_CHECK, + ) + return + _LOGGER.debug( "A quick_check is being performed on the sqlite3 database at %s", dbpath ) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index a4f70fc09a6..6116b383341 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -304,6 +304,7 @@ def test_recorder_setup_failure(): db_retry_wait=3, entity_filter=CONFIG_SCHEMA({DOMAIN: {}}), exclude_t=[], + db_integrity_check=False, ) rec.start() rec.join() diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 14b26b8c8e3..23ab7ff929d 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -69,29 +69,77 @@ def test_recorder_bad_execute(hass_recorder): assert e_mock.call_count == 2 -def test_validate_or_move_away_sqlite_database(hass, tmpdir, caplog): - """Ensure a malformed sqlite database is moved away.""" +def test_validate_or_move_away_sqlite_database_with_integrity_check( + hass, tmpdir, caplog +): + """Ensure a malformed sqlite database is moved away. + + A quick_check is run here + """ + + db_integrity_check = True test_dir = tmpdir.mkdir("test_validate_or_move_away_sqlite_database") test_db_file = f"{test_dir}/broken.db" dburl = f"{SQLITE_URL_PREFIX}{test_db_file}" - util.validate_sqlite_database(test_db_file) is True + util.validate_sqlite_database(test_db_file, db_integrity_check) is True assert os.path.exists(test_db_file) is True - assert util.validate_or_move_away_sqlite_database(dburl) is False + assert ( + util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False + ) _corrupt_db_file(test_db_file) - assert util.validate_sqlite_database(dburl) is False + assert util.validate_sqlite_database(dburl, db_integrity_check) is False - assert util.validate_or_move_away_sqlite_database(dburl) is False + assert ( + util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False + ) assert "corrupt or malformed" in caplog.text - assert util.validate_sqlite_database(dburl) is False + assert util.validate_sqlite_database(dburl, db_integrity_check) is False - assert util.validate_or_move_away_sqlite_database(dburl) is True + assert util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is True + + +def test_validate_or_move_away_sqlite_database_without_integrity_check( + hass, tmpdir, caplog +): + """Ensure a malformed sqlite database is moved away. + + The quick_check is skipped, but we can still find + corruption if the whole database is unreadable + """ + + db_integrity_check = False + + test_dir = tmpdir.mkdir("test_validate_or_move_away_sqlite_database") + test_db_file = f"{test_dir}/broken.db" + dburl = f"{SQLITE_URL_PREFIX}{test_db_file}" + + util.validate_sqlite_database(test_db_file, db_integrity_check) is True + + assert os.path.exists(test_db_file) is True + assert ( + util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False + ) + + _corrupt_db_file(test_db_file) + + assert util.validate_sqlite_database(dburl, db_integrity_check) is False + + assert ( + util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is False + ) + + assert "corrupt or malformed" in caplog.text + + assert util.validate_sqlite_database(dburl, db_integrity_check) is False + + assert util.validate_or_move_away_sqlite_database(dburl, db_integrity_check) is True def test_last_run_was_recently_clean(hass_recorder): @@ -134,25 +182,32 @@ def test_combined_checks(hass_recorder): """Run Checks on the open database.""" hass = hass_recorder() + db_integrity_check = False + cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor() - assert util.run_checks_on_open_db("fake_db_path", cursor) is None + assert ( + util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check) is None + ) # We are patching recorder.util here in order # to avoid creating the full database on disk with patch("homeassistant.components.recorder.util.last_run_was_recently_clean"): - assert util.run_checks_on_open_db("fake_db_path", cursor) is None + assert ( + util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check) + is None + ) with patch( "homeassistant.components.recorder.util.last_run_was_recently_clean", side_effect=sqlite3.DatabaseError, ), pytest.raises(sqlite3.DatabaseError): - util.run_checks_on_open_db("fake_db_path", cursor) + util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check) cursor.execute("DROP TABLE events;") with pytest.raises(sqlite3.DatabaseError): - util.run_checks_on_open_db("fake_db_path", cursor) + util.run_checks_on_open_db("fake_db_path", cursor, db_integrity_check) def _corrupt_db_file(test_db_file): From 3e9963a216629026fe790ebd2087ed23cc3f9e05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2020 03:14:16 -0500 Subject: [PATCH 561/862] Overcome group concurrent setup limitation (#39483) With a lot of groups the limitation that groups had to be setup one at a time could account for the bulk of startup time. --- homeassistant/components/group/__init__.py | 48 +++++++++++++++----- tests/components/group/test_init.py | 51 ++++++++++++++++++++++ 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 1dc9a187030..cda49591d5c 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -334,18 +334,39 @@ async def async_setup(hass, config): async def _async_process_config(hass, config, component): """Process group configuration.""" + hass.data.setdefault(GROUP_ORDER, 0) + + tasks = [] + for object_id, conf in config.get(DOMAIN, {}).items(): name = conf.get(CONF_NAME, object_id) entity_ids = conf.get(CONF_ENTITIES) or [] icon = conf.get(CONF_ICON) mode = conf.get(CONF_ALL) - # Don't create tasks and await them all. The order is important as - # groups get a number based on creation order. - await Group.async_create_group( - hass, name, entity_ids, icon=icon, object_id=object_id, mode=mode + # We keep track of the order when we are creating the tasks + # in the same way that async_create_group does to make + # sure we use the same ordering system. This overcomes + # the problem with concurrently creating the groups + tasks.append( + Group.async_create_group( + hass, + name, + entity_ids, + icon=icon, + object_id=object_id, + mode=mode, + order=hass.data[GROUP_ORDER], + ) ) + # Keep track of the group order without iterating + # every state in the state machine every time + # we setup a new group + hass.data[GROUP_ORDER] += 1 + + await asyncio.gather(*tasks) + class GroupEntity(Entity): """Representation of a Group of entities.""" @@ -418,11 +439,12 @@ class Group(Entity): icon=None, object_id=None, mode=None, + order=None, ): """Initialize a group.""" return asyncio.run_coroutine_threadsafe( Group.async_create_group( - hass, name, entity_ids, user_defined, icon, object_id, mode + hass, name, entity_ids, user_defined, icon, object_id, mode, order ), hass.loop, ).result() @@ -436,28 +458,30 @@ class Group(Entity): icon=None, object_id=None, mode=None, + order=None, ): """Initialize a group. This method must be run in the event loop. """ - hass.data.setdefault(GROUP_ORDER, 0) + if order is None: + hass.data.setdefault(GROUP_ORDER, 0) + order = hass.data[GROUP_ORDER] + # Keep track of the group order without iterating + # every state in the state machine every time + # we setup a new group + hass.data[GROUP_ORDER] += 1 group = Group( hass, name, - order=hass.data[GROUP_ORDER], + order=order, icon=icon, user_defined=user_defined, entity_ids=entity_ids, mode=mode, ) - # Keep track of the group order without iterating - # every state in the state machine every time - # we setup a new group - hass.data[GROUP_ORDER] += 1 - group.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id or name, hass=hass ) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 3709c4856a2..9684c107bb7 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -512,3 +512,54 @@ async def test_group_order(hass): assert hass.states.get("group.group_zero").attributes["order"] == 0 assert hass.states.get("group.group_one").attributes["order"] == 1 assert hass.states.get("group.group_two").attributes["order"] == 2 + + +async def test_group_order_with_dynamic_creation(hass): + """Test that order gets incremented when creating a new group.""" + hass.states.async_set("light.bowl", STATE_ON) + + assert await async_setup_component( + hass, + "group", + { + "group": { + "group_zero": {"entities": "light.Bowl", "icon": "mdi:work"}, + "group_one": {"entities": "light.Bowl", "icon": "mdi:work"}, + "group_two": {"entities": "light.Bowl", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("group.group_zero").attributes["order"] == 0 + assert hass.states.get("group.group_one").attributes["order"] == 1 + assert hass.states.get("group.group_two").attributes["order"] == 2 + + await hass.services.async_call( + group.DOMAIN, + group.SERVICE_SET, + {"object_id": "new_group", "name": "New Group", "entities": "light.bowl"}, + ) + await hass.async_block_till_done() + + assert hass.states.get("group.new_group").attributes["order"] == 3 + + await hass.services.async_call( + group.DOMAIN, + group.SERVICE_REMOVE, + { + "object_id": "new_group", + }, + ) + await hass.async_block_till_done() + + assert not hass.states.get("group.new_group") + + await hass.services.async_call( + group.DOMAIN, + group.SERVICE_SET, + {"object_id": "new_group2", "name": "New Group 2", "entities": "light.bowl"}, + ) + await hass.async_block_till_done() + + assert hass.states.get("group.new_group2").attributes["order"] == 4 From e55a014e944ab42b153f04614abca74125df20fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2020 03:35:15 -0500 Subject: [PATCH 562/862] Undecorate RenderInfo result property (#39108) --- homeassistant/helpers/event.py | 2 +- homeassistant/helpers/template.py | 1 - tests/helpers/test_template.py | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 55bb68fc6ec..dcc05675a01 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -680,7 +680,7 @@ class _TrackTemplateResultInfo: info_changed = True try: - result: Union[str, TemplateError] = self._info[template].result + result: Union[str, TemplateError] = self._info[template].result() except TemplateError as ex: result = ex diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 4d559a57c1f..c4c8f3a02f0 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -175,7 +175,6 @@ class RenderInfo: split_entity_id(entity_id)[0] in self.domains or entity_id in self.entities ) - @property def result(self) -> str: """Results of the template computation.""" if self.exception is not None: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index b0643d1addf..5b32634d8f0 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -51,7 +51,7 @@ def extract_entities(hass, template_str, variables=None): def assert_result_info(info, result, entities=None, domains=None, all_states=False): """Check result info.""" - assert info.result == result + assert info.result() == result assert info.all_states == all_states assert info.filter_lifecycle("invalid_entity_name.somewhere") == all_states if entities is not None: @@ -96,7 +96,7 @@ def test_invalid_template(hass): info = tmpl.async_render_to_info() with pytest.raises(TemplateError): - assert info.result == "impossible" + assert info.result() == "impossible" tmpl = template.Template("{{states(keyword)}}", hass) From 4c6960ed36dd1c141f96f1bf9d4cfb20caf4c45b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Sep 2020 10:52:33 +0200 Subject: [PATCH 563/862] Fix discovery update of MQTT light (#39325) --- .../components/mqtt/light/__init__.py | 8 +- .../components/mqtt/light/schema_basic.py | 104 +++++++++--------- .../components/mqtt/light/schema_json.py | 2 +- .../components/mqtt/light/schema_template.py | 2 +- tests/components/mqtt/test_common.py | 35 +++++- tests/components/mqtt/test_light.py | 23 +++- 6 files changed, 105 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index d48b4ae4762..fe33756e51e 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -39,7 +39,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT light through configuration.yaml.""" - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -51,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -63,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up a MQTT Light.""" setup_entity = { @@ -72,5 +72,5 @@ async def _async_setup_entity( "template": async_setup_entity_template, } await setup_entity[config[CONF_SCHEMA]]( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 03b0646307f..54af71b3e05 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -29,21 +29,12 @@ from homeassistant.components.mqtt import ( subscription, ) from homeassistant.const import ( - CONF_BRIGHTNESS, - CONF_COLOR_TEMP, CONF_DEVICE, - CONF_EFFECT, - CONF_HS, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, - CONF_RGB, - CONF_STATE, CONF_UNIQUE_ID, - CONF_VALUE_TEMPLATE, - CONF_WHITE_VALUE, - CONF_XY, STATE_ON, ) from homeassistant.core import callback @@ -97,6 +88,18 @@ DEFAULT_ON_COMMAND_TYPE = "last" VALUES_ON_COMMAND_TYPE = ["first", "last", "brightness"] +COMMAND_TEMPLATE_KEYS = [CONF_COLOR_TEMP_COMMAND_TEMPLATE, CONF_RGB_COMMAND_TEMPLATE] +VALUE_TEMPLATE_KEYS = [ + CONF_BRIGHTNESS_VALUE_TEMPLATE, + CONF_COLOR_TEMP_VALUE_TEMPLATE, + CONF_EFFECT_VALUE_TEMPLATE, + CONF_HS_VALUE_TEMPLATE, + CONF_RGB_VALUE_TEMPLATE, + CONF_STATE_VALUE_TEMPLATE, + CONF_WHITE_VALUE_TEMPLATE, + CONF_XY_VALUE_TEMPLATE, +] + PLATFORM_SCHEMA_BASIC = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { @@ -151,12 +154,10 @@ PLATFORM_SCHEMA_BASIC = ( async def async_setup_entity_basic( - config, async_add_entities, config_entry, discovery_data=None + hass, config, async_add_entities, config_entry, discovery_data=None ): """Set up a MQTT Light.""" - config.setdefault(CONF_STATE_VALUE_TEMPLATE, config.get(CONF_VALUE_TEMPLATE)) - - async_add_entities([MqttLight(config, config_entry, discovery_data)]) + async_add_entities([MqttLight(hass, config, config_entry, discovery_data)]) class MqttLight( @@ -169,8 +170,9 @@ class MqttLight( ): """Representation of a MQTT light.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT light.""" + self.hass = hass self._state = False self._sub_state = None self._brightness = None @@ -181,7 +183,8 @@ class MqttLight( self._topic = None self._payload = None - self._templates = None + self._command_templates = None + self._value_templates = None self._optimistic = False self._optimistic_rgb = False self._optimistic_brightness = False @@ -244,20 +247,24 @@ class MqttLight( } self._topic = topic self._payload = {"on": config[CONF_PAYLOAD_ON], "off": config[CONF_PAYLOAD_OFF]} - self._templates = { - CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE), - CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE), - CONF_COLOR_TEMP_COMMAND_TEMPLATE: config.get( - CONF_COLOR_TEMP_COMMAND_TEMPLATE - ), - CONF_EFFECT: config.get(CONF_EFFECT_VALUE_TEMPLATE), - CONF_HS: config.get(CONF_HS_VALUE_TEMPLATE), - CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE), - CONF_RGB_COMMAND_TEMPLATE: config.get(CONF_RGB_COMMAND_TEMPLATE), - CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), - CONF_WHITE_VALUE: config.get(CONF_WHITE_VALUE_TEMPLATE), - CONF_XY: config.get(CONF_XY_VALUE_TEMPLATE), - } + + value_templates = {} + for key in VALUE_TEMPLATE_KEYS: + value_templates[key] = lambda value: value + for key in VALUE_TEMPLATE_KEYS & config.keys(): + tpl = config[key] + value_templates[key] = tpl.async_render_with_possible_json_value + tpl.hass = self.hass + self._value_templates = value_templates + + command_templates = {} + for key in COMMAND_TEMPLATE_KEYS: + command_templates[key] = None + for key in COMMAND_TEMPLATE_KEYS & config.keys(): + tpl = config[key] + command_templates[key] = tpl.async_render + tpl.hass = self.hass + self._command_templates = command_templates optimistic = config[CONF_OPTIMISTIC] self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None @@ -286,13 +293,6 @@ class MqttLight( async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} - templates = {} - for key, tpl in list(self._templates.items()): - if tpl is None: - templates[key] = lambda value: value - else: - tpl.hass = self.hass - templates[key] = tpl.async_render_with_possible_json_value last_state = await self.async_get_last_state() @@ -300,7 +300,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new MQTT messages.""" - payload = templates[CONF_STATE](msg.payload) + payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) return @@ -324,7 +324,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def brightness_received(msg): """Handle new MQTT messages for the brightness.""" - payload = templates[CONF_BRIGHTNESS](msg.payload) + payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty brightness message from '%s'", msg.topic) return @@ -356,7 +356,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def rgb_received(msg): """Handle new MQTT messages for RGB.""" - payload = templates[CONF_RGB](msg.payload) + payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty rgb message from '%s'", msg.topic) return @@ -388,7 +388,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def color_temp_received(msg): """Handle new MQTT messages for color temperature.""" - payload = templates[CONF_COLOR_TEMP](msg.payload) + payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic) return @@ -418,7 +418,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def effect_received(msg): """Handle new MQTT messages for effect.""" - payload = templates[CONF_EFFECT](msg.payload) + payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty effect message from '%s'", msg.topic) return @@ -448,7 +448,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def hs_received(msg): """Handle new MQTT messages for hs color.""" - payload = templates[CONF_HS](msg.payload) + payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic) return @@ -480,7 +480,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def white_value_received(msg): """Handle new MQTT messages for white value.""" - payload = templates[CONF_WHITE_VALUE](msg.payload) + payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty white value message from '%s'", msg.topic) return @@ -512,7 +512,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def xy_received(msg): """Handle new MQTT messages for xy color.""" - payload = templates[CONF_XY](msg.payload) + payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload) if not payload: _LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic) return @@ -692,11 +692,9 @@ class MqttLight( rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100 ) - tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] + tpl = self._command_templates[CONF_RGB_COMMAND_TEMPLATE] if tpl: - rgb_color_str = tpl.async_render( - {"red": rgb[0], "green": rgb[1], "blue": rgb[2]} - ) + rgb_color_str = tpl({"red": rgb[0], "green": rgb[1], "blue": rgb[2]}) else: rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" @@ -772,11 +770,9 @@ class MqttLight( rgb = color_util.color_hsv_to_RGB( self._hs[0], self._hs[1], kwargs[ATTR_BRIGHTNESS] / 255 * 100 ) - tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] + tpl = self._command_templates[CONF_RGB_COMMAND_TEMPLATE] if tpl: - rgb_color_str = tpl.async_render( - {"red": rgb[0], "green": rgb[1], "blue": rgb[2]} - ) + rgb_color_str = tpl({"red": rgb[0], "green": rgb[1], "blue": rgb[2]}) else: rgb_color_str = f"{rgb[0]},{rgb[1]},{rgb[2]}" @@ -797,10 +793,10 @@ class MqttLight( and self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None ): color_temp = int(kwargs[ATTR_COLOR_TEMP]) - tpl = self._templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE] + tpl = self._command_templates[CONF_COLOR_TEMP_COMMAND_TEMPLATE] if tpl: - color_temp = tpl.async_render({"value": color_temp}) + color_temp = tpl({"value": color_temp}) mqtt.async_publish( self.hass, diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 4c8b9c41405..39ea550d877 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -125,7 +125,7 @@ PLATFORM_SCHEMA_JSON = ( async def async_setup_entity_json( - config: ConfigType, async_add_entities, config_entry, discovery_data + hass, config: ConfigType, async_add_entities, config_entry, discovery_data ): """Set up a MQTT JSON Light.""" async_add_entities([MqttLightJson(config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 0c512f0e1a3..9c2758524a3 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -98,7 +98,7 @@ PLATFORM_SCHEMA_TEMPLATE = ( async def async_setup_entity_template( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ): """Set up a MQTT Template light.""" async_add_entities([MqttLightTemplate(config, config_entry, discovery_data)]) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 547d8adad9e..75e1c12a46c 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -503,7 +503,20 @@ async def help_test_discovery_removal(hass, mqtt_mock, caplog, domain, data): assert state is None -async def help_test_discovery_update(hass, mqtt_mock, caplog, domain, data1, data2): +async def help_test_discovery_update( + hass, + mqtt_mock, + caplog, + domain, + discovery_data1, + discovery_data2, + state_data1=None, + state_data2=None, + state1=None, + state2=None, + attributes1=None, + attributes2=None, +): """Test update of discovered component. This is a test helper for the MqttDiscoveryUpdate mixin. @@ -511,19 +524,35 @@ async def help_test_discovery_update(hass, mqtt_mock, caplog, domain, data1, dat entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] await async_start(hass, "homeassistant", entry) - async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data1) await hass.async_block_till_done() + if state_data1: + for (topic, data) in state_data1: + async_fire_mqtt_message(hass, topic, data) state = hass.states.get(f"{domain}.beer") assert state is not None assert state.name == "Beer" + if state1: + assert state.state == state1 + if attributes1: + for (attr, value) in attributes1: + assert state.attributes.get(attr) == value - async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data2) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data2) await hass.async_block_till_done() + if state_data2: + for (topic, data) in state_data2: + async_fire_mqtt_message(hass, topic, data) state = hass.states.get(f"{domain}.beer") assert state is not None assert state.name == "Milk" + if state2: + assert state.state == state2 + if attributes2: + for (attr, value) in attributes2: + assert state.attributes.get(attr) == value state = hass.states.get(f"{domain}.milk") assert state is None diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 75d3e694838..d83cd7fda7f 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -716,9 +716,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + await common.async_turn_on(hass, "light.test", white_value=80, color_temp=125) mqtt_mock.async_publish.assert_has_calls( [ @@ -728,6 +727,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): call("test_light_rgb/hs/set", "359.0,78.0", 2, False), call("test_light_rgb/white_value/set", 80, 2, False), call("test_light_rgb/xy/set", "0.14,0.131", 2, False), + call("test_light_rgb/color_temp/set", 125, 2, False), ], any_order=True, ) @@ -1439,15 +1439,26 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): data1 = ( '{ "name": "Beer",' ' "state_topic": "test_topic",' - ' "command_topic": "test_topic" }' + ' "command_topic": "test_topic",' + ' "state_value_template": "{{value_json.power1}}" }' ) data2 = ( '{ "name": "Milk",' ' "state_topic": "test_topic",' - ' "command_topic": "test_topic" }' + ' "command_topic": "test_topic",' + ' "state_value_template": "{{value_json.power2}}" }' ) await help_test_discovery_update( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, + mqtt_mock, + caplog, + light.DOMAIN, + data1, + data2, + state_data1=[("test_topic", '{"power1":"ON"}')], + state1="on", + state_data2=[("test_topic", '{"power2":"OFF"}')], + state2="off", ) From 603707aa851f2bced23efe511911f5812bdf0767 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 2 Sep 2020 10:57:12 +0200 Subject: [PATCH 564/862] Add Sonos media browser capability (#39239) --- .../components/media_player/const.py | 3 + homeassistant/components/sonos/const.py | 8 + .../components/sonos/media_player.py | 309 +++++++++++++++++- 3 files changed, 302 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 651dcd94eff..64d77a4889e 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -43,8 +43,11 @@ MEDIA_TYPE_APP = "app" MEDIA_TYPE_ALBUM = "album" MEDIA_TYPE_TRACK = "track" MEDIA_TYPE_ARTIST = "artist" +MEDIA_TYPE_CONTRIBUTING_ARTIST = "contributing_artist" MEDIA_TYPE_PODCAST = "podcast" MEDIA_TYPE_SEASON = "season" +MEDIA_TYPE_GENRE = "genre" +MEDIA_TYPE_COMPOSER = "composer" SERVICE_CLEAR_PLAYLIST = "clear_playlist" SERVICE_PLAY_MEDIA = "play_media" diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 0d88249f740..da397b3e5e7 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -2,3 +2,11 @@ DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" + +SONOS_ARTIST = "artists" +SONOS_ALBUM = "albums" +SONOS_PLAYLISTS = "playlists" +SONOS_GENRE = "genres" +SONOS_ALBUM_ARTIST = "album_artists" +SONOS_TRACKS = "tracks" +SONOS_COMPOSER = "composers" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index a2440802139..11c42e072fd 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -4,6 +4,7 @@ import datetime import functools as ft import logging import socket +import urllib.parse import async_timeout import pysonos @@ -16,8 +17,15 @@ import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_COMPOSER, + MEDIA_TYPE_CONTRIBUTING_ARTIST, + MEDIA_TYPE_GENRE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TRACK, + SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -31,6 +39,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) +from homeassistant.components.media_player.errors import BrowseError from homeassistant.const import ( ATTR_TIME, EVENT_HOMEASSISTANT_STOP, @@ -43,12 +52,17 @@ from homeassistant.helpers import config_validation as cv, entity_platform, serv import homeassistant.helpers.device_registry as dr from homeassistant.util.dt import utcnow -from . import ( - CONF_ADVERTISE_ADDR, - CONF_HOSTS, - CONF_INTERFACE_ADDR, +from . import CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR +from .const import ( DATA_SONOS, DOMAIN as SONOS_DOMAIN, + SONOS_ALBUM, + SONOS_ALBUM_ARTIST, + SONOS_ARTIST, + SONOS_COMPOSER, + SONOS_GENRE, + SONOS_PLAYLISTS, + SONOS_TRACKS, ) _LOGGER = logging.getLogger(__name__) @@ -57,23 +71,102 @@ SCAN_INTERVAL = 10 DISCOVERY_INTERVAL = 60 SUPPORT_SONOS = ( - SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_MUTE - | SUPPORT_PLAY - | SUPPORT_PAUSE - | SUPPORT_STOP - | SUPPORT_SELECT_SOURCE - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_SEEK - | SUPPORT_PLAY_MEDIA - | SUPPORT_SHUFFLE_SET + SUPPORT_BROWSE_MEDIA | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SEEK + | SUPPORT_SELECT_SOURCE + | SUPPORT_SHUFFLE_SET + | SUPPORT_STOP + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET ) SOURCE_LINEIN = "Line-in" SOURCE_TV = "TV" +EXPANDABLE_MEDIA_TYPES = [ + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_COMPOSER, + MEDIA_TYPE_GENRE, + MEDIA_TYPE_PLAYLIST, + SONOS_ALBUM, + SONOS_ALBUM_ARTIST, + SONOS_ARTIST, + SONOS_GENRE, + SONOS_COMPOSER, + SONOS_PLAYLISTS, +] + +SONOS_TO_MEDIA_TYPES = { + SONOS_ALBUM: MEDIA_TYPE_ALBUM, + SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST, + SONOS_ARTIST: MEDIA_TYPE_CONTRIBUTING_ARTIST, + SONOS_COMPOSER: MEDIA_TYPE_COMPOSER, + SONOS_GENRE: MEDIA_TYPE_GENRE, + SONOS_PLAYLISTS: MEDIA_TYPE_PLAYLIST, + SONOS_TRACKS: MEDIA_TYPE_TRACK, + "object.container.album.musicAlbum": MEDIA_TYPE_ALBUM, + "object.container.genre.musicGenre": MEDIA_TYPE_PLAYLIST, + "object.container.person.composer": MEDIA_TYPE_PLAYLIST, + "object.container.person.musicArtist": MEDIA_TYPE_ARTIST, + "object.container.playlistContainer.sameArtist": MEDIA_TYPE_ARTIST, + "object.container.playlistContainer": MEDIA_TYPE_PLAYLIST, + "object.item.audioItem.musicTrack": MEDIA_TYPE_TRACK, +} + +MEDIA_TYPES_TO_SONOS = { + MEDIA_TYPE_ALBUM: SONOS_ALBUM, + MEDIA_TYPE_ARTIST: SONOS_ALBUM_ARTIST, + MEDIA_TYPE_CONTRIBUTING_ARTIST: SONOS_ARTIST, + MEDIA_TYPE_COMPOSER: SONOS_COMPOSER, + MEDIA_TYPE_GENRE: SONOS_GENRE, + MEDIA_TYPE_PLAYLIST: SONOS_PLAYLISTS, + MEDIA_TYPE_TRACK: SONOS_TRACKS, +} + +SONOS_TYPES_MAPPING = { + "A:ALBUM": SONOS_ALBUM, + "A:ALBUMARTIST": SONOS_ALBUM_ARTIST, + "A:ARTIST": SONOS_ARTIST, + "A:COMPOSER": SONOS_COMPOSER, + "A:GENRE": SONOS_GENRE, + "A:PLAYLISTS": SONOS_PLAYLISTS, + "A:TRACKS": SONOS_TRACKS, + "object.container.album.musicAlbum": SONOS_ALBUM, + "object.container.genre.musicGenre": SONOS_GENRE, + "object.container.person.composer": SONOS_COMPOSER, + "object.container.person.musicArtist": SONOS_ALBUM_ARTIST, + "object.container.playlistContainer.sameArtist": SONOS_ARTIST, + "object.container.playlistContainer": SONOS_PLAYLISTS, + "object.item.audioItem.musicTrack": SONOS_TRACKS, +} + +LIBRARY_TITLES_MAPPING = { + "A:ALBUM": "Albums", + "A:ALBUMARTIST": "Artists", + "A:ARTIST": "Contributing Artists", + "A:COMPOSER": "Composers", + "A:GENRE": "Genres", + "A:PLAYLISTS": "Playlists", + "A:TRACKS": "Tracks", +} + +PLAYABLE_MEDIA_TYPES = [ + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_COMPOSER, + MEDIA_TYPE_CONTRIBUTING_ARTIST, + MEDIA_TYPE_GENRE, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TRACK, +] + ATTR_SONOS_GROUP = "sonos_group" UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"] @@ -384,6 +477,7 @@ class SonosEntity(MediaPlayerEntity): self._sonos_group = [self] self._status = None self._uri = None + self._media_library = pysonos.music_library.MusicLibrary(self.soco) self._media_duration = None self._media_position = None self._media_position_updated_at = None @@ -648,9 +742,10 @@ class SonosEntity(MediaPlayerEntity): self._clear_media_position() try: - library = pysonos.music_library.MusicLibrary(self.soco) album_art_uri = variables["current_track_meta_data"].album_art_uri - self._media_image_url = library.build_album_art_full_uri(album_art_uri) + self._media_image_url = self._media_library.build_album_art_full_uri( + album_art_uri + ) except (TypeError, KeyError, AttributeError): pass @@ -1014,7 +1109,7 @@ class SonosEntity(MediaPlayerEntity): If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ - if media_type == MEDIA_TYPE_MUSIC: + if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): if kwargs.get(ATTR_MEDIA_ENQUEUE): try: self.soco.add_uri_to_queue(media_id) @@ -1028,6 +1123,10 @@ class SonosEntity(MediaPlayerEntity): else: self.soco.play_uri(media_id) elif media_type == MEDIA_TYPE_PLAYLIST: + if media_id.startswith("S:"): + item = get_media(self._media_library, media_id, media_type) + self.soco.play_uri(item.get_uri()) + return try: playlists = self.soco.get_sonos_playlists() playlist = next(p for p in playlists if p.title == media_id) @@ -1036,6 +1135,14 @@ class SonosEntity(MediaPlayerEntity): self.soco.play_from_queue(0) except StopIteration: _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) + elif media_type in PLAYABLE_MEDIA_TYPES: + item = get_media(self._media_library, media_id, media_type) + + if not item: + _LOGGER.error('Could not find "%s" in the library', media_id) + return + + self.soco.play_uri(item.get_uri()) else: _LOGGER.error('Sonos does not support a media type of "%s"', media_type) @@ -1284,3 +1391,169 @@ class SonosEntity(MediaPlayerEntity): attributes[ATTR_QUEUE_POSITION] = self.queue_position return attributes + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + if media_content_type in [None, "library"]: + return await self.hass.async_add_executor_job( + library_payload, self._media_library + ) + + payload = { + "search_type": media_content_type, + "idstring": media_content_id, + } + response = await self.hass.async_add_executor_job( + build_item_response, self._media_library, payload + ) + if response is None: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) + return response + + +def build_item_response(media_library, payload): + """Create response payload for the provided media query.""" + if payload["search_type"] == MEDIA_TYPE_ALBUM and payload["idstring"].startswith( + ("A:GENRE", "A:COMPOSER") + ): + payload["idstring"] = "A:ALBUMARTIST/" + "/".join( + payload["idstring"].split("/")[2:] + ) + + media = media_library.browse_by_idstring( + MEDIA_TYPES_TO_SONOS[payload["search_type"]], + payload["idstring"], + full_album_art_uri=True, + max_items=0, + ) + + if media is None: + return + + thumbnail = None + title = None + + # Fetch album info for titles and thumbnails + # Can't be extracted from track info + if ( + payload["search_type"] == MEDIA_TYPE_ALBUM + and media[0].item_class == "object.item.audioItem.musicTrack" + ): + item = get_media(media_library, payload["idstring"], SONOS_ALBUM_ARTIST) + title = getattr(item, "title", None) + thumbnail = getattr(item, "album_art_uri", media[0].album_art_uri) + + if not title: + try: + title = urllib.parse.unquote(payload["idstring"].split("/")[1]) + except IndexError: + title = LIBRARY_TITLES_MAPPING[payload["idstring"]] + + return { + "title": title, + "thumbnail": thumbnail, + "media_content_id": payload["idstring"], + "media_content_type": payload["search_type"], + "children": [item_payload(item) for item in media], + "can_play": can_play(payload["search_type"]), + "can_expand": can_expand(payload["search_type"]), + } + + +def item_payload(item): + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + return { + "title": item.title, + "thumbnail": getattr(item, "album_art_uri", None), + "media_content_id": get_content_id(item), + "media_content_type": SONOS_TO_MEDIA_TYPES[get_media_type(item)], + "can_play": can_play(item.item_class), + "can_expand": can_expand(item), + } + + +def library_payload(media_library): + """ + Create response payload to describe contents of a specific library. + + Used by async_browse_media. + """ + return { + "title": "Music Library", + "media_content_id": "library", + "media_content_type": "library", + "can_play": False, + "can_expand": True, + "children": [item_payload(item) for item in media_library.browse()], + } + + +def get_media_type(item): + """Extract media type of item.""" + if item.item_class == "object.item.audioItem.musicTrack": + return SONOS_TRACKS + + if ( + item.item_class == "object.container.album.musicAlbum" + and SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0]) + in [ + SONOS_ALBUM_ARTIST, + SONOS_GENRE, + ] + ): + return SONOS_TYPES_MAPPING[item.item_class] + + return SONOS_TYPES_MAPPING.get(item.item_id.split("/")[0], item.item_class) + + +def can_play(item): + """ + Test if playable. + + Used by async_browse_media. + """ + return SONOS_TO_MEDIA_TYPES.get(item) in PLAYABLE_MEDIA_TYPES + + +def can_expand(item): + """ + Test if expandable. + + Used by async_browse_media. + """ + if isinstance(item, str): + return SONOS_TYPES_MAPPING.get(item) in EXPANDABLE_MEDIA_TYPES + + if SONOS_TO_MEDIA_TYPES.get(item.item_class) in EXPANDABLE_MEDIA_TYPES: + return True + + return SONOS_TYPES_MAPPING.get(item.item_id) in EXPANDABLE_MEDIA_TYPES + + +def get_content_id(item): + """Extract content id or uri.""" + if item.item_class == "object.item.audioItem.musicTrack": + return item.get_uri() + return item.item_id + + +def get_media(media_library, item_id, search_type): + """Fetch media/album.""" + search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) + + if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: + item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) + + for item in media_library.browse_by_idstring( + search_type, + "/".join(item_id.split("/")[:-1]), + full_album_art_uri=True, + ): + if item.item_id == item_id: + return item From 4486251382e1ac4c92361a321adc596b9f991d97 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 2 Sep 2020 04:05:14 -0500 Subject: [PATCH 565/862] Add max_exceeded log level option to automations & scripts (#39448) --- .../components/automation/__init__.py | 2 + homeassistant/components/script/__init__.py | 2 + homeassistant/helpers/script.py | 19 ++++++- tests/helpers/test_script.py | 54 +++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 746ab3adc03..db97c3a321a 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -39,6 +39,7 @@ from homeassistant.helpers.script import ( ATTR_MAX, ATTR_MODE, CONF_MAX, + CONF_MAX_EXCEEDED, SCRIPT_MODE_SINGLE, Script, make_script_schema, @@ -515,6 +516,7 @@ async def _async_process_config(hass, config, component): running_description="automation actions", script_mode=config_block[CONF_MODE], max_runs=config_block[CONF_MAX], + max_exceeded=config_block[CONF_MAX_EXCEEDED], logger=_LOGGER, ) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index b6d02872adb..20f12361621 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.script import ( ATTR_MAX, ATTR_MODE, CONF_MAX, + CONF_MAX_EXCEEDED, SCRIPT_MODE_SINGLE, Script, make_script_schema, @@ -260,6 +261,7 @@ class ScriptEntity(ToggleEntity): change_listener=self.async_change_listener, script_mode=cfg[CONF_MODE], max_runs=cfg[CONF_MAX], + max_exceeded=cfg[CONF_MAX_EXCEEDED], logger=logging.getLogger(f"{__name__}.{object_id}"), ) self._changed = asyncio.Event() diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index adbacda6742..d4d9c11fa71 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -24,6 +24,7 @@ import voluptuous as vol from homeassistant import exceptions import homeassistant.components.device_automation as device_automation +from homeassistant.components.logger import LOGSEVERITY import homeassistant.components.scene as scene from homeassistant.const import ( ATTR_ENTITY_ID, @@ -88,6 +89,10 @@ DEFAULT_SCRIPT_MODE = SCRIPT_MODE_SINGLE CONF_MAX = "max" DEFAULT_MAX = 10 +CONF_MAX_EXCEEDED = "max_exceeded" +_MAX_EXCEEDED_CHOICES = list(LOGSEVERITY) + ["SILENT"] +DEFAULT_MAX_EXCEEDED = "WARNING" + ATTR_CUR = "current" ATTR_MAX = "max" ATTR_MODE = "mode" @@ -113,6 +118,9 @@ def make_script_schema(schema, default_script_mode, extra=vol.PREVENT_EXTRA): vol.Optional(CONF_MAX, default=DEFAULT_MAX): vol.All( vol.Coerce(int), vol.Range(min=2) ), + vol.Optional(CONF_MAX_EXCEEDED, default=DEFAULT_MAX_EXCEEDED): vol.All( + vol.Upper, vol.In(_MAX_EXCEEDED_CHOICES) + ), }, extra=extra, ) @@ -710,6 +718,7 @@ class Script: change_listener: Optional[Callable[..., Any]] = None, script_mode: str = DEFAULT_SCRIPT_MODE, max_runs: int = DEFAULT_MAX, + max_exceeded: str = DEFAULT_MAX_EXCEEDED, logger: Optional[logging.Logger] = None, log_exceptions: bool = True, top_level: bool = True, @@ -743,6 +752,7 @@ class Script: self._runs: List[_ScriptRun] = [] self.max_runs = max_runs + self._max_exceeded = max_exceeded if script_mode == SCRIPT_MODE_QUEUED: self._queue_lck = asyncio.Lock() self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {} @@ -871,13 +881,18 @@ class Script: if self.is_running: if self.script_mode == SCRIPT_MODE_SINGLE: - self._log("Already running", level=logging.WARNING) + if self._max_exceeded != "SILENT": + self._log("Already running", level=LOGSEVERITY[self._max_exceeded]) return if self.script_mode == SCRIPT_MODE_RESTART: self._log("Restarting") await self.async_stop(update_state=False) elif len(self._runs) == self.max_runs: - self._log("Maximum number of runs exceeded", level=logging.WARNING) + if self._max_exceeded != "SILENT": + self._log( + "Maximum number of runs exceeded", + level=LOGSEVERITY[self._max_exceeded], + ) return # If this is a top level Script then make a copy of the variables in case they diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 364d635f506..e997e3e92d6 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1419,6 +1419,60 @@ async def test_script_mode_single(hass, caplog): assert events[1].data["value"] == 2 +@pytest.mark.parametrize("max_exceeded", [None, "WARNING", "INFO", "ERROR", "SILENT"]) +@pytest.mark.parametrize( + "script_mode,max_runs", [("single", 1), ("parallel", 2), ("queued", 2)] +) +async def test_max_exceeded(hass, caplog, max_exceeded, script_mode, max_runs): + """Test max_exceeded option.""" + sequence = cv.SCRIPT_SCHEMA( + {"wait_template": "{{ states.switch.test.state == 'off' }}"} + ) + if max_exceeded is None: + script_obj = script.Script( + hass, + sequence, + "Test Name", + "test_domain", + script_mode=script_mode, + max_runs=max_runs, + ) + else: + script_obj = script.Script( + hass, + sequence, + "Test Name", + "test_domain", + script_mode=script_mode, + max_runs=max_runs, + max_exceeded=max_exceeded, + ) + hass.states.async_set("switch.test", "on") + for _ in range(max_runs + 1): + hass.async_create_task(script_obj.async_run(context=Context())) + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() + if max_exceeded is None: + max_exceeded = "WARNING" + if max_exceeded == "SILENT": + assert not any( + any( + message in rec.message + for message in ("Already running", "Maximum number of runs exceeded") + ) + for rec in caplog.records + ) + else: + assert any( + rec.levelname == max_exceeded + and any( + message in rec.message + for message in ("Already running", "Maximum number of runs exceeded") + ) + for rec in caplog.records + ) + + @pytest.mark.parametrize( "script_mode,messages,last_events", [("restart", ["Restarting"], [2]), ("parallel", [], [2, 2])], From d5bcafaefdb2fc180c775ac9157f5900bbccc17a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Sep 2020 11:30:37 +0200 Subject: [PATCH 566/862] Handle Alexa entity removed (#39569) --- homeassistant/components/alexa/entities.py | 7 ++++++- homeassistant/components/alexa/state_report.py | 5 ++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index e70da218e47..bd68e4ad926 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -201,6 +201,11 @@ class DisplayCategory: WEARABLE = "WEARABLE" +def generate_alexa_id(entity_id: str) -> str: + """Return the alexa ID for an entity ID.""" + return entity_id.replace(".", "#").translate(TRANSLATION_TABLE) + + class AlexaEntity: """An adaptation of an entity, expressed in Alexa's terms. @@ -232,7 +237,7 @@ class AlexaEntity: def alexa_id(self): """Return the Alexa API entity id.""" - return self.entity.entity_id.replace(".", "#").translate(TRANSLATION_TABLE) + return generate_alexa_id(self.entity.entity_id) def display_categories(self): """Return a list of display categories.""" diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 6c9b9ac5180..a61dfc02d10 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -10,7 +10,7 @@ from homeassistant.const import MATCH_ALL, STATE_ON import homeassistant.util.dt as dt_util from .const import API_CHANGE, Cause -from .entities import ENTITY_ADAPTERS +from .entities import ENTITY_ADAPTERS, generate_alexa_id from .messages import AlexaResponse _LOGGER = logging.getLogger(__name__) @@ -181,8 +181,7 @@ async def async_send_delete_message(hass, config, entity_ids): if domain not in ENTITY_ADAPTERS: continue - alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) - endpoints.append({"endpointId": alexa_entity.alexa_id()}) + endpoints.append({"endpointId": generate_alexa_id(entity_id)}) payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}} From 78b0837cb81f1a44ab45203164b4a89cbb17f04e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Sep 2020 11:50:00 +0200 Subject: [PATCH 567/862] Remove flaky wol test that didn't test anything (#39571) --- tests/components/wake_on_lan/test_switch.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index b6747062ce5..4eae15911c2 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -101,16 +101,6 @@ class TestWolSwitch(unittest.TestCase): state = self.hass.states.get("switch.wake_on_lan") assert STATE_ON == state.state - @patch("wakeonlan.send_magic_packet", new=send_magic_packet) - @patch("subprocess.call", new=call) - def test_minimal_config(self): - """Test with minimal config.""" - assert setup_component( - self.hass, - switch.DOMAIN, - {"switch": {"platform": "wake_on_lan", "mac": "00-01-02-03-04-05"}}, - ) - @patch("wakeonlan.send_magic_packet", new=send_magic_packet) @patch("subprocess.call", new=call) def test_broadcast_config_ip_and_port(self): From 9f5baa0bf70ab6e80c9140d4865742ff3a9dc0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 2 Sep 2020 12:50:57 +0300 Subject: [PATCH 568/862] Syncthru device registry (#36750) * Store printer instances in hass.data * Add SyncThru device registry support * Use config entry id as hass.data key Co-authored-by: Paulus Schoutsen * Use printer hostname as device registry name * Handle non-syncthru device more gracefully on entry setup * Use device identifiers rather than connections to link entities with devices Co-authored-by: Paulus Schoutsen --- homeassistant/components/syncthru/__init__.py | 63 ++++++++++++++++++- .../components/syncthru/exceptions.py | 7 --- homeassistant/components/syncthru/sensor.py | 32 ++++------ 3 files changed, 72 insertions(+), 30 deletions(-) delete mode 100644 homeassistant/components/syncthru/exceptions.py diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 512db1b7527..83d32eb9b47 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -1,23 +1,82 @@ """The syncthru component.""" +import logging +from typing import Set, Tuple + +from pysyncthru import SyncThru + from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, device_registry as dr from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up.""" + hass.data.setdefault(DOMAIN, {}) return True async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up config entry.""" + + session = aiohttp_client.async_get_clientsession(hass) + printer = hass.data[DOMAIN][entry.entry_id] = SyncThru( + entry.data[CONF_URL], session + ) + + try: + await printer.update() + except ValueError: + _LOGGER.error( + "Device at %s not appear to be a SyncThru printer, aborting setup", + printer.url, + ) + return False + else: + if printer.is_unknown_state(): + raise ConfigEntryNotReady + + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + connections=device_connections(printer), + identifiers=device_identifiers(printer), + model=printer.model(), + name=printer.hostname(), + ) + hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, SENSOR_DOMAIN) ) return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload the config entry.""" - return await hass.config_entries.async_forward_entry_unload(entry, SENSOR_DOMAIN) + await hass.config_entries.async_forward_entry_unload(entry, SENSOR_DOMAIN) + hass.data[DOMAIN].pop(entry.entry_id, None) + return True + + +def device_identifiers(printer: SyncThru) -> Set[Tuple[str, str]]: + """Get device identifiers for device registry.""" + return {(DOMAIN, printer.serial_number())} + + +def device_connections(printer: SyncThru) -> Set[Tuple[str, str]]: + """Get device connections for device registry.""" + connections = set() + try: + mac = printer.raw()["identity"]["mac_addr"] + if mac: + connections.add((dr.CONNECTION_NETWORK_MAC, mac)) + except AttributeError: + pass + return connections diff --git a/homeassistant/components/syncthru/exceptions.py b/homeassistant/components/syncthru/exceptions.py deleted file mode 100644 index 0bb4b8229ce..00000000000 --- a/homeassistant/components/syncthru/exceptions.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Samsung SyncThru exceptions.""" - -from homeassistant.exceptions import HomeAssistantError - - -class SyncThruNotSupported(HomeAssistantError): - """Error to indicate SyncThru is not supported.""" diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index c9aa6d8de9c..869a2a8e997 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -8,13 +8,11 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_RESOURCE, CONF_URL, UNIT_PERCENTAGE -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from . import device_identifiers from .const import DEFAULT_MODEL, DEFAULT_NAME_TEMPLATE, DOMAIN -from .exceptions import SyncThruNotSupported _LOGGER = logging.getLogger(__name__) @@ -61,25 +59,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up from config entry.""" - session = aiohttp_client.async_get_clientsession(hass) + printer = hass.data[DOMAIN][config_entry.entry_id] - printer = SyncThru(config_entry.data[CONF_URL], session) - # Test if the discovered device actually is a syncthru printer - # and fetch the available toner/drum/etc - try: - # No error is thrown when the device is off - # (only after user added it manually) - # therefore additional catches are inside the Sensor below - await printer.update() - supp_toner = printer.toner_status(filter_supported=True) - supp_drum = printer.drum_status(filter_supported=True) - supp_tray = printer.input_tray_status(filter_supported=True) - supp_output_tray = printer.output_tray_status() - except ValueError as ex: - raise SyncThruNotSupported from ex - else: - if printer.is_unknown_state(): - raise PlatformNotReady + supp_toner = printer.toner_status(filter_supported=True) + supp_drum = printer.drum_status(filter_supported=True) + supp_tray = printer.input_tray_status(filter_supported=True) + supp_output_tray = printer.output_tray_status() name = config_entry.data[CONF_NAME] devices = [SyncThruMainSensor(printer, name)] @@ -140,6 +125,11 @@ class SyncThruSensor(Entity): """Return the state attributes of the device.""" return self._attributes + @property + def device_info(self): + """Return device information.""" + return {"identifiers": device_identifiers(self.syncthru)} + class SyncThruMainSensor(SyncThruSensor): """Implementation of the main sensor, conducting the actual polling.""" From 7ff633f531b9b0f75189f7ef778b76492b8ae81e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 2 Sep 2020 05:55:10 -0400 Subject: [PATCH 569/862] Automatically update app list for Vizio SmartTV's (#38641) --- homeassistant/components/vizio/__init__.py | 55 ++++++++++++++- homeassistant/components/vizio/config_flow.py | 11 ++- homeassistant/components/vizio/const.py | 5 +- homeassistant/components/vizio/manifest.json | 2 +- .../components/vizio/media_player.py | 33 +++++++-- homeassistant/components/vizio/services.yaml | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vizio/conftest.py | 15 ++-- tests/components/vizio/const.py | 16 ++++- tests/components/vizio/test_config_flow.py | 69 +++++++++++++------ tests/components/vizio/test_init.py | 8 ++- tests/components/vizio/test_media_player.py | 52 ++++++++++++-- 13 files changed, 225 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index a52b395c5c9..25960da72cf 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1,15 +1,24 @@ """The vizio component.""" import asyncio +from datetime import timedelta +import logging +from typing import Any, Dict, List +from pyvizio.const import APPS +from pyvizio.util import gen_apps_list_from_url import voluptuous as vol from homeassistant.components.media_player import DEVICE_CLASS_TV -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_IMPORT, ConfigEntry from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_APPS, CONF_DEVICE_CLASS, DOMAIN, VIZIO_SCHEMA +_LOGGER = logging.getLogger(__name__) + def validate_apps(config: ConfigType) -> ConfigType: """Validate CONF_APPS is only used when CONF_DEVICE_CLASS == DEVICE_CLASS_TV.""" @@ -47,6 +56,16 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" + + hass.data.setdefault(DOMAIN, {}) + if ( + CONF_APPS not in hass.data[DOMAIN] + and config_entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + ): + coordinator = VizioAppsDataUpdateCoordinator(hass) + await coordinator.async_refresh() + hass.data[DOMAIN][CONF_APPS] = coordinator + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, platform) @@ -68,4 +87,38 @@ async def async_unload_entry( ) ) + # Exclude this config entry because its not unloaded yet + if not any( + entry.state == ENTRY_STATE_LOADED + and entry.entry_id != config_entry.entry_id + and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + for entry in hass.config_entries.async_entries(DOMAIN) + ): + hass.data[DOMAIN].pop(CONF_APPS) + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return unload_ok + + +class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Vizio app config data.""" + + def __init__(self, hass: HomeAssistantType) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(days=1), + update_method=self._async_update_data, + ) + self.data = APPS + + async def _async_update_data(self) -> List[Dict[str, Any]]: + """Update data via library.""" + data = await gen_apps_list_from_url(session=async_get_clientsession(self.hass)) + if not data: + raise UpdateFailed + return sorted(data, key=lambda app: app["name"]) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index ba7bb39f7ea..74fc0d746e3 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -5,6 +5,7 @@ import socket from typing import Any, Dict, Optional from pyvizio import VizioAsync, async_guess_device_type +from pyvizio.const import APP_HOME import voluptuous as vol from homeassistant import config_entries @@ -154,7 +155,15 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): default=self.config_entry.options.get(CONF_APPS, {}).get( default_include_or_exclude, [] ), - ): cv.multi_select(VizioAsync.get_apps_list()), + ): cv.multi_select( + [ + APP_HOME["name"], + *[ + app["name"] + for app in self.hass.data[DOMAIN][CONF_APPS].data + ], + ] + ), } ) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index e0b60769e45..bcfc38950d3 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -1,5 +1,4 @@ """Constants used by vizio component.""" -from pyvizio import VizioAsync from pyvizio.const import ( DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, @@ -101,10 +100,10 @@ VIZIO_SCHEMA = { vol.Optional(CONF_APPS): vol.All( { vol.Exclusive(CONF_INCLUDE, "apps_filter"): vol.All( - cv.ensure_list, [vol.All(cv.string, vol.In(VizioAsync.get_apps_list()))] + cv.ensure_list, [cv.string] ), vol.Exclusive(CONF_EXCLUDE, "apps_filter"): vol.All( - cv.ensure_list, [vol.All(cv.string, vol.In(VizioAsync.get_apps_list()))] + cv.ensure_list, [cv.string] ), vol.Optional(CONF_ADDITIONAL_CONFIGS): vol.All( cv.ensure_list, diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index a0c36dc0089..7aa544b4a0b 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "VIZIO SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.51"], + "requirements": ["pyvizio==0.1.56"], "codeowners": ["@raman325"], "config_flow": true, "zeroconf": ["_viziocast._tcp.local."], diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index cbddbaa6897..ab5386c151b 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -5,10 +5,11 @@ from typing import Any, Callable, Dict, List, Optional, Union from pyvizio import VizioAsync from pyvizio.api.apps import find_app_name -from pyvizio.const import APP_HOME, APPS, INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP +from pyvizio.const import APP_HOME, INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP from homeassistant.components.media_player import ( DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV, SUPPORT_SELECT_SOUND_MODE, MediaPlayerEntity, ) @@ -23,6 +24,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -32,6 +34,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_ADDITIONAL_CONFIGS, @@ -78,6 +81,7 @@ async def async_setup_entry( params = {} if not config_entry.options: params["options"] = {CONF_VOLUME_STEP: volume_step} + include_or_exclude_key = next( ( key @@ -115,7 +119,9 @@ async def async_setup_entry( _LOGGER.warning("Failed to connect to %s", host) raise PlatformNotReady - entity = VizioDevice(config_entry, device, name, device_class) + apps_coordinator = hass.data[DOMAIN].get(CONF_APPS) + + entity = VizioDevice(config_entry, device, name, device_class, apps_coordinator) async_add_entities([entity], update_before_add=True) platform = entity_platform.current_platform.get() @@ -133,10 +139,12 @@ class VizioDevice(MediaPlayerEntity): device: VizioAsync, name: str, device_class: str, + apps_coordinator: DataUpdateCoordinator, ) -> None: """Initialize Vizio device.""" self._config_entry = config_entry self._async_unsub_listeners = [] + self._apps_coordinator = apps_coordinator self._name = name self._state = None @@ -150,6 +158,7 @@ class VizioDevice(MediaPlayerEntity): self._available_sound_modes = [] self._available_inputs = [] self._available_apps = [] + self._all_apps = apps_coordinator.data if apps_coordinator else None self._conf_apps = config_entry.options.get(CONF_APPS, {}) self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get( CONF_ADDITIONAL_CONFIGS, [] @@ -255,14 +264,15 @@ class VizioDevice(MediaPlayerEntity): # Create list of available known apps from known app list after # filtering by CONF_INCLUDE/CONF_EXCLUDE - self._available_apps = self._apps_list(self._device.get_apps_list()) + self._available_apps = self._apps_list([app["name"] for app in self._all_apps]) self._current_app_config = await self._device.get_current_app_config( log_api_exception=False ) self._current_app = find_app_name( - self._current_app_config, [APP_HOME, *APPS, *self._additional_app_configs] + self._current_app_config, + [APP_HOME, *self._all_apps, *self._additional_app_configs], ) if self._current_app == NO_APP_RUNNING: @@ -286,6 +296,7 @@ class VizioDevice(MediaPlayerEntity): async def _async_update_options(self, config_entry: ConfigEntry) -> None: """Update options if the update signal comes from this entity.""" self._volume_step = config_entry.options[CONF_VOLUME_STEP] + # Update so that CONF_ADDITIONAL_CONFIGS gets retained for imports self._conf_apps.update(config_entry.options.get(CONF_APPS, {})) async def async_update_setting( @@ -314,6 +325,18 @@ class VizioDevice(MediaPlayerEntity): ) ) + # Register callback for app list updates if device is a TV + @callback + def apps_list_update(): + """Update list of all apps.""" + self._all_apps = self._apps_coordinator.data + self.async_write_ha_state() + + if self._device_class == DEVICE_CLASS_TV: + self._async_unsub_listeners.append( + self._apps_coordinator.async_add_listener(apps_list_update) + ) + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks when entity is removed.""" for listener in self._async_unsub_listeners: @@ -479,7 +502,7 @@ class VizioDevice(MediaPlayerEntity): ) ) elif source in self._available_apps: - await self._device.launch_app(source) + await self._device.launch_app(source, self._all_apps) async def async_volume_up(self) -> None: """Increase volume of the device.""" diff --git a/homeassistant/components/vizio/services.yaml b/homeassistant/components/vizio/services.yaml index c652b622de0..50bde6cab78 100644 --- a/homeassistant/components/vizio/services.yaml +++ b/homeassistant/components/vizio/services.yaml @@ -2,7 +2,7 @@ update_setting: description: Update the value of a setting on a particular Vizio media player device. fields: entity_id: - description: Name of an entity to send command. + description: Name of an entity to send command to. example: "media_player.vizio_smartcast" setting_type: description: The type of setting to be changed. Available types are listed in the `setting_types` property. diff --git a/requirements_all.txt b/requirements_all.txt index 64287ff4d7f..e6a49977d10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1830,7 +1830,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.51 +pyvizio==0.1.56 # homeassistant.components.velux pyvlx==0.2.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bafccebb99c..c0354d60ade 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -857,7 +857,7 @@ pyvera==0.3.9 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.51 +pyvizio==0.1.56 # homeassistant.components.volumio pyvolumio==0.1.2 diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 492494fc75e..e1f5dfa18b0 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -1,5 +1,6 @@ """Configure py.test.""" import pytest +from pyvizio.api.apps import AppConfig from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME from .const import ( @@ -57,6 +58,15 @@ def vizio_get_unique_id_fixture(): yield +@pytest.fixture(name="vizio_data_coordinator_update", autouse=True) +def vizio_data_coordinator_update_fixture(): + """Mock get data coordinator update.""" + with patch( + "homeassistant.components.vizio.gen_apps_list_from_url", return_value=APP_LIST, + ): + yield + + @pytest.fixture(name="vizio_no_unique_id") def vizio_no_unique_id_fixture(): """Mock no vizio unique ID returrned.""" @@ -191,15 +201,12 @@ def vizio_update_with_apps_fixture(vizio_update: pytest.fixture): with patch( "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", return_value=get_mock_inputs(INPUT_LIST_WITH_APPS), - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_apps_list", - return_value=APP_LIST, ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", return_value="CAST", ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", - return_value=CURRENT_APP_CONFIG, + return_value=AppConfig(**CURRENT_APP_CONFIG), ): yield diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index 43605de67ad..6ec746b2c54 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -72,7 +72,21 @@ INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"] CURRENT_APP = "Hulu" CURRENT_APP_CONFIG = {CONF_APP_ID: "3", CONF_NAME_SPACE: 4, CONF_MESSAGE: None} -APP_LIST = ["Hulu", "Netflix"] +APP_LIST = [ + { + "name": "Hulu", + "country": ["*"], + "id": ["1"], + "config": [{"NAME_SPACE": 4, "APP_ID": "3", "MESSAGE": None}], + }, + { + "name": "Netflix", + "country": ["*"], + "id": ["2"], + "config": [{"NAME_SPACE": 1, "APP_ID": "2", "MESSAGE": None}], + }, +] +APP_NAME_LIST = [app["name"] for app in APP_LIST] INPUT_LIST_WITH_APPS = INPUT_LIST + ["CAST"] CUSTOM_CONFIG = {CONF_APP_ID: "test", CONF_MESSAGE: None, CONF_NAME_SPACE: 10} ADDITIONAL_APP_CONFIG = { diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 41490e9f4be..2506a685e43 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -109,12 +109,18 @@ async def test_user_flow_all_fields( assert CONF_APPS not in result["data"] -async def test_speaker_options_flow(hass: HomeAssistantType) -> None: +async def test_speaker_options_flow( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: """Test options config flow for speaker.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_SPEAKER_CONFIG) - entry.add_to_hass(hass) - - assert not entry.options + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) @@ -131,12 +137,18 @@ async def test_speaker_options_flow(hass: HomeAssistantType) -> None: assert CONF_APPS not in result["data"] -async def test_tv_options_flow_no_apps(hass: HomeAssistantType) -> None: +async def test_tv_options_flow_no_apps( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: """Test options config flow for TV without providing apps option.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) - entry.add_to_hass(hass) - - assert not entry.options + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) @@ -156,12 +168,18 @@ async def test_tv_options_flow_no_apps(hass: HomeAssistantType) -> None: assert CONF_APPS not in result["data"] -async def test_tv_options_flow_with_apps(hass: HomeAssistantType) -> None: +async def test_tv_options_flow_with_apps( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: """Test options config flow for TV with providing apps option.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) - entry.add_to_hass(hass) - - assert not entry.options + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) @@ -182,14 +200,23 @@ async def test_tv_options_flow_with_apps(hass: HomeAssistantType) -> None: assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} -async def test_tv_options_flow_start_with_volume(hass: HomeAssistantType) -> None: +async def test_tv_options_flow_start_with_volume( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: """Test options config flow for TV with providing apps option after providing volume step in initial config.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=MOCK_USER_VALID_TV_CONFIG, - options={CONF_VOLUME_STEP: VOLUME_STEP}, + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - entry.add_to_hass(hass) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = result["result"] + + result = await hass.config_entries.options.async_init( + entry.entry_id, data={CONF_VOLUME_STEP: VOLUME_STEP} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert entry.options assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP} diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index 1be067e9570..715800d006d 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.components.vizio.const import DOMAIN from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_setup_component from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID @@ -37,7 +38,12 @@ async def test_load_and_unload( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert DOMAIN in hass.data + assert "apps" in hass.data[DOMAIN] + assert isinstance(hass.data[DOMAIN]["apps"], DataUpdateCoordinator) - assert await hass.config_entries.async_unload(config_entry.entry_id) + assert await config_entry.async_unload(hass) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 + assert "apps" not in hass.data.get(DOMAIN, {}) + assert DOMAIN not in hass.data diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 6b19089057b..3dc093d38ea 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -8,6 +8,7 @@ import pytest from pytest import raises from pyvizio.api.apps import AppConfig from pyvizio.const import ( + APPS, DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, INPUT_APPS, @@ -51,6 +52,7 @@ from homeassistant.util import dt as dt_util from .const import ( ADDITIONAL_APP_CONFIG, APP_LIST, + APP_NAME_LIST, CURRENT_APP, CURRENT_APP_CONFIG, CURRENT_EQ, @@ -358,7 +360,11 @@ async def test_services( await _test_service(hass, MP_DOMAIN, "pow_on", SERVICE_TURN_ON, None) await _test_service(hass, MP_DOMAIN, "pow_off", SERVICE_TURN_OFF, None) await _test_service( - hass, MP_DOMAIN, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} + hass, + MP_DOMAIN, + "mute_on", + SERVICE_VOLUME_MUTE, + {ATTR_MEDIA_VOLUME_MUTED: True}, ) await _test_service( hass, @@ -511,7 +517,7 @@ async def test_setup_with_apps( hass, MOCK_USER_VALID_TV_CONFIG, CURRENT_APP_CONFIG ): attr = hass.states.get(ENTITY_ID).attributes - _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_LIST), attr) + _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr) assert CURRENT_APP in attr["source_list"] assert attr["source"] == CURRENT_APP assert attr["app_name"] == CURRENT_APP @@ -524,6 +530,7 @@ async def test_setup_with_apps( SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: CURRENT_APP}, CURRENT_APP, + APP_LIST, ) @@ -580,13 +587,13 @@ async def test_setup_with_apps_additional_apps_config( _assert_source_list_with_apps( list( INPUT_LIST_WITH_APPS - + APP_LIST + + APP_NAME_LIST + [ app["name"] for app in MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG[CONF_APPS][ CONF_ADDITIONAL_CONFIGS ] - if app["name"] not in APP_LIST + if app["name"] not in APP_NAME_LIST ] ), attr, @@ -603,6 +610,7 @@ async def test_setup_with_apps_additional_apps_config( SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "Netflix"}, "Netflix", + APP_LIST, ) await _test_service( hass, @@ -649,7 +657,7 @@ async def test_setup_with_unknown_app_config( hass, MOCK_USER_VALID_TV_CONFIG, UNKNOWN_APP_CONFIG ): attr = hass.states.get(ENTITY_ID).attributes - _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_LIST), attr) + _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr) assert attr["source"] == UNKNOWN_APP assert attr["app_name"] == UNKNOWN_APP assert attr["app_id"] == UNKNOWN_APP_CONFIG @@ -666,7 +674,7 @@ async def test_setup_with_no_running_app( hass, MOCK_USER_VALID_TV_CONFIG, vars(AppConfig()) ): attr = hass.states.get(ENTITY_ID).attributes - _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_LIST), attr) + _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr) assert attr["source"] == "CAST" assert "app_id" not in attr assert "app_name" not in attr @@ -694,3 +702,35 @@ async def test_setup_tv_without_mute( _assert_sources_and_volume(attr, VIZIO_DEVICE_CLASS_TV) assert "sound_mode" not in attr assert "is_volume_muted" not in attr + + +async def test_apps_update( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update_with_apps: pytest.fixture, + caplog: pytest.fixture, +) -> None: + """Test device setup with apps where no app is running.""" + with patch( + "homeassistant.components.vizio.gen_apps_list_from_url", return_value=None, + ): + async with _cm_for_test_setup_tv_with_apps( + hass, MOCK_USER_VALID_TV_CONFIG, vars(AppConfig()) + ): + # Check source list, remove TV inputs, and verify that the integration is + # using the default APPS list + sources = hass.states.get(ENTITY_ID).attributes["source_list"] + apps = list(set(sources) - set(INPUT_LIST)) + assert len(apps) == len(APPS) + + with patch( + "homeassistant.components.vizio.gen_apps_list_from_url", + return_value=APP_LIST, + ): + async_fire_time_changed(hass, dt_util.now() + timedelta(days=2)) + await hass.async_block_till_done() + # Check source list, remove TV inputs, and verify that the integration is + # now using the APP_LIST list + sources = hass.states.get(ENTITY_ID).attributes["source_list"] + apps = list(set(sources) - set(INPUT_LIST)) + assert len(apps) == len(APP_LIST) From ec6a1f91376966a4161512c59d4ed32039e90acc Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 2 Sep 2020 11:59:51 +0200 Subject: [PATCH 570/862] Add support for receiver and speaker device classes (#38381) These are now officially supported by google --- homeassistant/components/google_assistant/const.py | 3 +++ homeassistant/components/media_player/__init__.py | 3 ++- tests/components/google_assistant/test_smart_home.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 88b704eb518..339f4ab27ab 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -80,6 +80,7 @@ TYPE_ALARM = f"{PREFIX_TYPES}SECURITYSYSTEM" TYPE_SETTOP = f"{PREFIX_TYPES}SETTOP" TYPE_HUMIDIFIER = f"{PREFIX_TYPES}HUMIDIFIER" TYPE_DEHUMIDIFIER = f"{PREFIX_TYPES}DEHUMIDIFIER" +TYPE_RECEIVER = f"{PREFIX_TYPES}AUDIO_VIDEO_RECEIVER" SERVICE_REQUEST_SYNC = "request_sync" HOMEGRAPH_URL = "https://homegraph.googleapis.com/" @@ -144,6 +145,8 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR, (media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV, + (media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER, + (media_player.DOMAIN, media_player.DEVICE_CLASS_RECEIVER): TYPE_RECEIVER, (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR, (sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY): TYPE_SENSOR, (humidifier.DOMAIN, humidifier.DEVICE_CLASS_HUMIDIFIER): TYPE_HUMIDIFIER, diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 70eda4329c6..886f0170a06 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -127,8 +127,9 @@ SCAN_INTERVAL = timedelta(seconds=10) DEVICE_CLASS_TV = "tv" DEVICE_CLASS_SPEAKER = "speaker" +DEVICE_CLASS_RECEIVER = "receiver" -DEVICE_CLASSES = [DEVICE_CLASS_TV, DEVICE_CLASS_SPEAKER] +DEVICE_CLASSES = [DEVICE_CLASS_TV, DEVICE_CLASS_SPEAKER, DEVICE_CLASS_RECEIVER] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index cf25c79efdb..1aadb39d5a8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -853,6 +853,8 @@ async def test_device_class_cover(hass, device_class, google_type): [ ("non_existing_class", "action.devices.types.SETTOP"), ("tv", "action.devices.types.TV"), + ("speaker", "action.devices.types.SPEAKER"), + ("receiver", "action.devices.types.AUDIO_VIDEO_RECEIVER"), ], ) async def test_device_media_player(hass, device_class, google_type): From 0892acbabd24cdd04e8aa91e2d058a7f462e1303 Mon Sep 17 00:00:00 2001 From: Michael Thingnes Date: Thu, 3 Sep 2020 00:11:13 +1200 Subject: [PATCH 571/862] Met.no migrate from classic to complete endpoint (#39493) --- CODEOWNERS | 2 +- homeassistant/components/met/__init__.py | 5 +- homeassistant/components/met/const.py | 183 +++++++++++++++++- homeassistant/components/met/manifest.json | 4 +- homeassistant/components/met/weather.py | 68 +++++-- .../components/norway_air/manifest.json | 2 +- homeassistant/components/weather/__init__.py | 15 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 257 insertions(+), 26 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0bb10972910..0f0757cb983 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -246,7 +246,7 @@ homeassistant/components/mcp23017/* @jardiamj homeassistant/components/mediaroom/* @dgomes homeassistant/components/melcloud/* @vilppuvuorinen homeassistant/components/melissa/* @kennedyshead -homeassistant/components/met/* @danielhiversen +homeassistant/components/met/* @danielhiversen @thimic homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/metoffice/* @MrHarcombe diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 1f5502170e4..4eedfc0b76d 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -22,9 +22,6 @@ import homeassistant.util.dt as dt_util from .const import CONF_TRACK_HOME, DOMAIN -URL = "https://api.met.no/weatherapi/locationforecast/2.0/classic" - - _LOGGER = logging.getLogger(__name__) @@ -142,7 +139,7 @@ class MetWeatherData: } self._weather_data = metno.MetWeatherData( - coordinates, async_get_clientsession(self.hass), URL + coordinates, async_get_clientsession(self.hass) ) async def fetch_data(self): diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index f29f034df68..8c507eb0b8d 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -1,7 +1,34 @@ """Constants for Met component.""" import logging -from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_FOG, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_SUNNY, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_ATTRIBUTION, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, +) DOMAIN = "met" @@ -11,4 +38,158 @@ CONF_TRACK_HOME = "track_home" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_{HOME_LOCATION_NAME}" +CONDITIONS_MAP = { + ATTR_CONDITION_CLEAR_NIGHT: {"clearsky_night"}, + ATTR_CONDITION_CLOUDY: {"cloudy_night", "cloudy_day", "cloudy"}, + ATTR_CONDITION_FOG: {"fog", "fog_day", "fog_night"}, + ATTR_CONDITION_LIGHTNING_RAINY: { + "heavyrainandthunder", + "heavyrainandthunder_day", + "heavyrainandthunder_night", + "heavyrainshowersandthunder", + "heavyrainshowersandthunder_day", + "heavyrainshowersandthunder_night", + "heavysleetandthunder", + "heavysleetandthunder_day", + "heavysleetandthunder_night", + "heavysleetshowersandthunder", + "heavysleetshowersandthunder_day", + "heavysleetshowersandthunder_night", + "heavysnowandthunder", + "heavysnowandthunder_day", + "heavysnowandthunder_night", + "heavysnowshowersandthunder", + "heavysnowshowersandthunder_day", + "heavysnowshowersandthunder_night", + "lightrainandthunder", + "lightrainandthunder_day", + "lightrainandthunder_night", + "lightrainshowersandthunder", + "lightrainshowersandthunder_day", + "lightrainshowersandthunder_night", + "lightsleetandthunder", + "lightsleetandthunder_day", + "lightsleetandthunder_night", + "lightsnowandthunder", + "lightsnowandthunder_day", + "lightsnowandthunder_night", + "lightssleetshowersandthunder", + "lightssleetshowersandthunder_day", + "lightssleetshowersandthunder_night", + "lightssnowshowersandthunder", + "lightssnowshowersandthunder_day", + "lightssnowshowersandthunder_night", + "rainandthunder", + "rainandthunder_day", + "rainandthunder_night", + "rainshowersandthunder", + "rainshowersandthunder_day", + "rainshowersandthunder_night", + "sleetandthunder", + "sleetandthunder_day", + "sleetandthunder_night", + "sleetshowersandthunder", + "sleetshowersandthunder_day", + "sleetshowersandthunder_night", + "snowshowersandthunder", + "snowshowersandthunder_day", + "snowshowersandthunder_night", + }, + ATTR_CONDITION_PARTLYCLOUDY: { + "fair", + "fair_day", + "fair_night", + "partlycloudy", + "partlycloudy_day", + "partlycloudy_night", + }, + ATTR_CONDITION_POURING: { + "heavyrain", + "heavyrain_day", + "heavyrain_night", + "heavyrainshowers", + "heavyrainshowers_day", + "heavyrainshowers_night", + }, + ATTR_CONDITION_RAINY: { + "lightrain", + "lightrain_day", + "lightrain_night", + "lightrainshowers", + "lightrainshowers_day", + "lightrainshowers_night", + "rain", + "rain_day", + "rain_night", + "rainshowers", + "rainshowers_day", + "rainshowers_night", + }, + ATTR_CONDITION_SNOWY: { + "heavysnow", + "heavysnow_day", + "heavysnow_night", + "heavysnowshowers", + "heavysnowshowers_day", + "heavysnowshowers_night", + "lightsnow", + "lightsnow_day", + "lightsnow_night", + "lightsnowshowers", + "lightsnowshowers_day", + "lightsnowshowers_night", + "snow", + "snow_day", + "snow_night", + "snowandthunder", + "snowandthunder_day", + "snowandthunder_night", + "snowshowers", + "snowshowers_day", + "snowshowers_night", + }, + ATTR_CONDITION_SNOWY_RAINY: { + "heavysleet", + "heavysleet_day", + "heavysleet_night", + "heavysleetshowers", + "heavysleetshowers_day", + "heavysleetshowers_night", + "lightsleet", + "lightsleet_day", + "lightsleet_night", + "lightsleetshowers", + "lightsleetshowers_day", + "lightsleetshowers_night", + "sleet", + "sleet_day", + "sleet_night", + "sleetshowers", + "sleetshowers_day", + "sleetshowers_night", + }, + ATTR_CONDITION_SUNNY: {"clearsky_day", "clearsky"}, +} + +FORECAST_MAP = { + ATTR_FORECAST_CONDITION: "condition", + ATTR_FORECAST_PRECIPITATION: "precipitation", + ATTR_FORECAST_PRECIPITATION_PROBABILITY: "precipitation_probability", + ATTR_FORECAST_TEMP: "temperature", + ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_TIME: "datetime", + ATTR_FORECAST_WIND_BEARING: "wind_bearing", + ATTR_FORECAST_WIND_SPEED: "wind_speed", +} + +ATTR_MAP = { + ATTR_WEATHER_ATTRIBUTION: "attribution", + ATTR_WEATHER_HUMIDITY: "humidity", + ATTR_WEATHER_PRESSURE: "pressure", + ATTR_WEATHER_TEMPERATURE: "temperature", + ATTR_WEATHER_VISIBILITY: "visibility", + ATTR_WEATHER_WIND_BEARING: "wind_bearing", + ATTR_WEATHER_WIND_SPEED: "wind_speed", +} + _LOGGER = logging.getLogger(".") diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 570e3df3bf0..38b77a0afd2 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -3,6 +3,6 @@ "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": ["pyMetno==0.7.1"], - "codeowners": ["@danielhiversen"] + "requirements": ["pyMetno==0.8.1"], + "codeowners": ["@danielhiversen", "@thimic"] } diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index e532c193e71..a53f66ab1dc 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -3,14 +3,23 @@ import logging import voluptuous as vol -from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, + PLATFORM_SCHEMA, + WeatherEntity, +) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_METERS, + LENGTH_KILOMETERS, LENGTH_MILES, PRESSURE_HPA, PRESSURE_INHG, @@ -21,7 +30,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure -from .const import CONF_TRACK_HOME, DOMAIN +from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP _LOGGER = logging.getLogger(__name__) @@ -78,6 +87,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) +def format_condition(condition: str) -> str: + """Return condition from dict CONDITIONS_MAP.""" + for key, value in CONDITIONS_MAP.items(): + if condition in value: + return key + return condition + + class MetWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met.no weather condition.""" @@ -118,12 +135,15 @@ class MetWeather(CoordinatorEntity, WeatherEntity): @property def condition(self): """Return the current condition.""" - return self.coordinator.data.current_weather_data.get("condition") + condition = self.coordinator.data.current_weather_data.get("condition") + return format_condition(condition) @property def temperature(self): """Return the temperature.""" - return self.coordinator.data.current_weather_data.get("temperature") + return self.coordinator.data.current_weather_data.get( + ATTR_MAP[ATTR_WEATHER_TEMPERATURE] + ) @property def temperature_unit(self): @@ -133,7 +153,9 @@ class MetWeather(CoordinatorEntity, WeatherEntity): @property def pressure(self): """Return the pressure.""" - pressure_hpa = self.coordinator.data.current_weather_data.get("pressure") + pressure_hpa = self.coordinator.data.current_weather_data.get( + ATTR_MAP[ATTR_WEATHER_PRESSURE] + ) if self._is_metric or pressure_hpa is None: return pressure_hpa @@ -142,23 +164,28 @@ class MetWeather(CoordinatorEntity, WeatherEntity): @property def humidity(self): """Return the humidity.""" - return self.coordinator.data.current_weather_data.get("humidity") + return self.coordinator.data.current_weather_data.get( + ATTR_MAP[ATTR_WEATHER_HUMIDITY] + ) @property def wind_speed(self): """Return the wind speed.""" - speed_m_s = self.coordinator.data.current_weather_data.get("wind_speed") - if self._is_metric or speed_m_s is None: - return speed_m_s + speed_km_h = self.coordinator.data.current_weather_data.get( + ATTR_MAP[ATTR_WEATHER_WIND_SPEED] + ) + if self._is_metric or speed_km_h is None: + return speed_km_h - speed_mi_s = convert_distance(speed_m_s, LENGTH_METERS, LENGTH_MILES) - speed_mi_h = speed_mi_s / 3600.0 + speed_mi_h = convert_distance(speed_km_h, LENGTH_KILOMETERS, LENGTH_MILES) return int(round(speed_mi_h)) @property def wind_bearing(self): """Return the wind direction.""" - return self.coordinator.data.current_weather_data.get("wind_bearing") + return self.coordinator.data.current_weather_data.get( + ATTR_MAP[ATTR_WEATHER_WIND_BEARING] + ) @property def attribution(self): @@ -169,5 +196,16 @@ class MetWeather(CoordinatorEntity, WeatherEntity): def forecast(self): """Return the forecast array.""" if self._hourly: - return self.coordinator.data.hourly_forecast - return self.coordinator.data.daily_forecast + met_forecast = self.coordinator.data.hourly_forecast + else: + met_forecast = self.coordinator.data.daily_forecast + ha_forecast = [] + for met_item in met_forecast: + ha_item = { + k: met_item[v] for k, v in FORECAST_MAP.items() if met_item.get(v) + } + ha_item[ATTR_FORECAST_CONDITION] = format_condition( + ha_item[ATTR_FORECAST_CONDITION] + ) + ha_forecast.append(ha_item) + return ha_forecast diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 96ff39bb6dd..193d96e2a18 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -2,6 +2,6 @@ "domain": "norway_air", "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": ["pyMetno==0.7.1"], + "requirements": ["pyMetno==0.8.1"], "codeowners": [] } diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 5a6fcc2d80b..8ddcf052e1f 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -16,6 +16,21 @@ from homeassistant.helpers.temperature import display_temp as show_temp _LOGGER = logging.getLogger(__name__) ATTR_CONDITION_CLASS = "condition_class" +ATTR_CONDITION_CLEAR_NIGHT = "clear-night" +ATTR_CONDITION_CLOUDY = "cloudy" +ATTR_CONDITION_EXCEPTIONAL = "exceptional" +ATTR_CONDITION_FOG = "fog" +ATTR_CONDITION_HAIL = "hail" +ATTR_CONDITION_LIGHTNING = "lightning" +ATTR_CONDITION_LIGHTNING_RAINY = "lightning-rainy" +ATTR_CONDITION_PARTLYCLOUDY = "partlycloudy" +ATTR_CONDITION_POURING = "pouring" +ATTR_CONDITION_RAINY = "rainy" +ATTR_CONDITION_SNOWY = "snowy" +ATTR_CONDITION_SNOWY_RAINY = "snowy-rainy" +ATTR_CONDITION_SUNNY = "sunny" +ATTR_CONDITION_WINDY = "windy" +ATTR_CONDITION_WINDY_VARIANT = "windy-variant" ATTR_FORECAST = "forecast" ATTR_FORECAST_CONDITION = "condition" ATTR_FORECAST_PRECIPITATION = "precipitation" diff --git a/requirements_all.txt b/requirements_all.txt index e6a49977d10..2e90608aada 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1186,7 +1186,7 @@ pyHS100==0.3.5.1 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.7.1 +pyMetno==0.8.1 # homeassistant.components.rfxtrx pyRFXtrx==0.25 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0354d60ade..0870058a2c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -577,7 +577,7 @@ pyHS100==0.3.5.1 # homeassistant.components.met # homeassistant.components.norway_air -pyMetno==0.7.1 +pyMetno==0.8.1 # homeassistant.components.rfxtrx pyRFXtrx==0.25 From 24f63127de52872b753015e426955069b90ce68c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 2 Sep 2020 14:16:23 +0200 Subject: [PATCH 572/862] Fix vizio black formatting (#39573) --- tests/components/vizio/conftest.py | 3 ++- tests/components/vizio/test_media_player.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index e1f5dfa18b0..08e5da5c9e5 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -62,7 +62,8 @@ def vizio_get_unique_id_fixture(): def vizio_data_coordinator_update_fixture(): """Mock get data coordinator update.""" with patch( - "homeassistant.components.vizio.gen_apps_list_from_url", return_value=APP_LIST, + "homeassistant.components.vizio.gen_apps_list_from_url", + return_value=APP_LIST, ): yield diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 3dc093d38ea..c4620b07025 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -712,7 +712,8 @@ async def test_apps_update( ) -> None: """Test device setup with apps where no app is running.""" with patch( - "homeassistant.components.vizio.gen_apps_list_from_url", return_value=None, + "homeassistant.components.vizio.gen_apps_list_from_url", + return_value=None, ): async with _cm_for_test_setup_tv_with_apps( hass, MOCK_USER_VALID_TV_CONFIG, vars(AppConfig()) From 6de02fc1b979e8d8061e0a3b53660e671932da46 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Sep 2020 14:53:07 +0200 Subject: [PATCH 573/862] Fix some more usages of asynctest (#39570) --- tests/components/command_line/test_cover.py | 3 ++- tests/components/deconz/test_config_flow.py | 3 ++- tests/components/group/test_light.py | 4 +--- tests/components/homeassistant/triggers/test_time_pattern.py | 2 +- tests/components/min_max/test_sensor.py | 3 +-- tests/components/universal/test_media_player.py | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index ceb3711f62b..508b1e1fb41 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -4,7 +4,6 @@ from os import path import tempfile from unittest import mock -from asynctest.mock import patch import pytest from homeassistant import config as hass_config @@ -19,6 +18,8 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from tests.async_mock import patch + @pytest.fixture def rs(hass): diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index b51869b01ab..43536a44bbe 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,7 +1,6 @@ """Tests for deCONZ config flow.""" import asyncio -from asynctest.mock import patch import pydeconz from homeassistant import data_entry_flow @@ -22,6 +21,8 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration +from tests.async_mock import patch + async def test_flow_discovered_bridges(hass, aioclient_mock): """Test that config flow works for discovered bridges.""" diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index dae975ead03..8855ca97626 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,8 +1,6 @@ """The tests for the Group Light platform.""" from os import path -from asynctest.mock import patch - from homeassistant import config as hass_config from homeassistant.components.group import DOMAIN, SERVICE_RELOAD import homeassistant.components.group.light as group @@ -34,7 +32,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import tests.async_mock -from tests.async_mock import MagicMock +from tests.async_mock import MagicMock, patch async def test_default_state(hass): diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index f2a2cd29d79..0ef071aadb6 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -1,5 +1,4 @@ """The tests for the time_pattern automation.""" -from asynctest.mock import patch import pytest import voluptuous as vol @@ -8,6 +7,7 @@ import homeassistant.components.homeassistant.triggers.time_pattern as time_patt from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed, async_mock_service, mock_component from tests.components.automation import common diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 8f532a6ec07..52de3dfa1ab 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -3,8 +3,6 @@ from os import path import statistics import unittest -from asynctest.mock import patch - from homeassistant import config as hass_config from homeassistant.components.min_max import DOMAIN from homeassistant.const import ( @@ -18,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component, setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 5333f72b2a9..38949c32ebb 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -4,7 +4,6 @@ from copy import copy from os import path import unittest -from asynctest.mock import patch from voluptuous.error import MultipleInvalid from homeassistant import config as hass_config @@ -23,6 +22,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant, mock_service From 70e39a26a29d165c77b12ae4775f4dcb459e68f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliv=C3=A9r=20Falvai?= Date: Wed, 2 Sep 2020 14:56:32 +0200 Subject: [PATCH 574/862] Fix UPC ConnectBox logout and device hostnames (#39568) --- homeassistant/components/upc_connect/device_tracker.py | 8 +++++--- homeassistant/components/upc_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index fc9225c6ef4..d68311a8793 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -69,8 +69,10 @@ class UPCDeviceScanner(DeviceScanner): async def async_get_device_name(self, device: str) -> Optional[str]: """Get the device name (the name of the wireless device not used).""" for connected_device in self.connect_box.devices: - if connected_device != device: - continue - return connected_device.hostname + if ( + connected_device.mac == device + and connected_device.hostname.lower() != "unknown" + ): + return connected_device.hostname return None diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index ebdcc630820..f34061e276a 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,6 +2,6 @@ "domain": "upc_connect", "name": "UPC Connect Box", "documentation": "https://www.home-assistant.io/integrations/upc_connect", - "requirements": ["connect-box==0.2.7"], + "requirements": ["connect-box==0.2.8"], "codeowners": ["@pvizeli", "@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2e90608aada..91c92a795ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,7 +436,7 @@ colorlog==4.2.1 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.7 +connect-box==0.2.8 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From 45c28dd9c565057bcf0c4bdb623f920373c61bca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2020 10:32:08 -0500 Subject: [PATCH 575/862] Provide a logbook option entity_matches_only to optimize for single entity lookup (#39555) * Provide a logbook option entity_matches_only to optimize for single entity id lookup When entity_matches_only is provided, contexts and events that do not contain the entity_id are not included in the logbook response. * Update homeassistant/components/logbook/__init__.py Co-authored-by: Paulus Schoutsen * api only takes a single entity Co-authored-by: Paulus Schoutsen --- homeassistant/components/logbook/__init__.py | 70 +++++++++---- tests/components/logbook/test_init.py | 101 ++++++++++++++++++- 2 files changed, 150 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 03dc1ffdef9..0c7786de90b 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -38,7 +38,13 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, callback, split_entity_id +from homeassistant.core import ( + DOMAIN as HA_DOMAIN, + callback, + split_entity_id, + valid_entity_id, +) +from homeassistant.exceptions import InvalidEntityFormatError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -51,6 +57,7 @@ from homeassistant.helpers.integration_platform import ( from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util +ENTITY_ID_JSON_TEMPLATE = '"entity_id": "{}"' ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": "([^"]+)"') DOMAIN_JSON_EXTRACT = re.compile('"domain": "([^"]+)"') @@ -87,7 +94,6 @@ ALL_EVENT_TYPES = [ SCRIPT_AUTOMATION_EVENTS = [EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED] - LOG_MESSAGE_SCHEMA = vol.Schema( { vol.Required(ATTR_NAME): cv.string, @@ -214,6 +220,8 @@ class LogbookView(HomeAssistantView): hass = request.app["hass"] + entity_matches_only = "entity_matches_only" in request.query + def json_events(): """Fetch events and generate JSON.""" return self.json( @@ -224,6 +232,7 @@ class LogbookView(HomeAssistantView): entity_id, self.filters, self.entities_filter, + entity_matches_only, ) ) @@ -390,11 +399,19 @@ def humanify(hass, events, entity_attr_cache, context_lookup): def _get_events( - hass, start_day, end_day, entity_id=None, filters=None, entities_filter=None + hass, + start_day, + end_day, + entity_id=None, + filters=None, + entities_filter=None, + entity_matches_only=False, ): """Get events for a period of time.""" entity_attr_cache = EntityAttributeCache(hass) context_lookup = {None: None} + entity_id_lower = None + apply_sql_entities_filter = True def yield_events(query): """Yield Events that are not filtered away.""" @@ -404,15 +421,17 @@ def _get_events( if _keep_event(hass, event, entities_filter): yield event - with session_scope(hass=hass) as session: - if entity_id is not None: - entity_ids = [entity_id.lower()] - entities_filter = generate_filter([], entity_ids, [], []) - apply_sql_entities_filter = False - else: - entity_ids = None - apply_sql_entities_filter = True + if entity_id is not None: + entity_id_lower = entity_id.lower() + if not valid_entity_id(entity_id_lower): + raise InvalidEntityFormatError( + f"Invalid entity id encountered: {entity_id_lower}. " + "Format should be ." + ) + entities_filter = generate_filter([], [entity_id_lower], [], []) + apply_sql_entities_filter = False + with session_scope(hass=hass) as session: old_state = aliased(States, name="old_state") query = ( @@ -458,14 +477,29 @@ def _get_events( .filter((Events.time_fired > start_day) & (Events.time_fired < end_day)) ) - if entity_ids: - query = query.filter( - ( - (States.last_updated == States.last_changed) - & States.entity_id.in_(entity_ids) + if entity_id_lower is not None: + if entity_matches_only: + # When entity_matches_only is provided, contexts and events that do not + # contain the entity_id are not included in the logbook response. + entity_id_json = ENTITY_ID_JSON_TEMPLATE.format(entity_id_lower) + query = query.filter( + ( + (States.last_updated == States.last_changed) + & (States.entity_id == entity_id_lower) + ) + | ( + States.state_id.is_(None) + & Events.event_data.contains(entity_id_json) + ) + ) + else: + query = query.filter( + ( + (States.last_updated == States.last_changed) + & (States.entity_id == entity_id_lower) + ) + | (States.state_id.is_(None)) ) - | (States.state_id.is_(None)) - ) else: query = query.filter( (States.last_updated == States.last_changed) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 4edf630322a..5e41f0bce89 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2021,7 +2021,7 @@ async def test_logbook_context_from_template(hass, hass_client): } }, ) - await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -2043,9 +2043,9 @@ async def test_logbook_context_from_template(hass, hass_client): ) await hass.async_block_till_done() - await hass.async_add_job(trigger_db_commit, hass) + await hass.async_add_executor_job(trigger_db_commit, hass) await hass.async_block_till_done() - await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) client = await hass_client() @@ -2081,6 +2081,101 @@ async def test_logbook_context_from_template(hass, hass_client): assert json_dict[5]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +async def test_logbook_entity_matches_only(hass, hass_client): + """Test the logbook view with a single entity and entity_matches_only.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", {}) + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ states.switch.test_state.state }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + } + }, + } + }, + ) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + # Entity added (should not be logged) + hass.states.async_set("switch.test_state", STATE_ON) + await hass.async_block_till_done() + + # First state change (should be logged) + hass.states.async_set("switch.test_state", STATE_OFF) + await hass.async_block_till_done() + + switch_turn_off_context = ha.Context( + id="9c5bd62de45711eaaeb351041eec8dd9", + user_id="9400facee45711eaa9308bfd3d19e474", + ) + hass.states.async_set( + "switch.test_state", STATE_ON, context=switch_turn_off_context + ) + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() + await hass.async_add_executor_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 with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test_state&entity_matches_only" + ) + assert response.status == 200 + json_dict = await response.json() + + assert len(json_dict) == 2 + + assert json_dict[0]["entity_id"] == "switch.test_state" + assert json_dict[0]["message"] == "turned off" + + assert json_dict[1]["entity_id"] == "switch.test_state" + assert json_dict[1]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + assert json_dict[1]["message"] == "turned on" + + +async def test_logbook_invalid_entity(hass, hass_client): + """Test the logbook view with requesting an invalid entity.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", {}) + await hass.async_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 with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=invalid&entity_matches_only" + ) + assert response.status == 500 + + class MockLazyEventPartialState(ha.Event): """Minimal mock of a Lazy event.""" From dfa6f0223a223e425d1d0d450743f7b314ee50cd Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Wed, 2 Sep 2020 19:07:27 +0300 Subject: [PATCH 576/862] library version upgrade to 0.46 (#39580) --- homeassistant/components/dynalite/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index 9c733ccc7a2..387e69a1fbd 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -4,5 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.45"] + "requirements": ["dynalite_devices==0.1.46"] } diff --git a/requirements_all.txt b/requirements_all.txt index 91c92a795ec..83fd8bef72b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -513,7 +513,7 @@ dwdwfsapi==1.0.2 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.45 +dynalite_devices==0.1.46 # homeassistant.components.rainforest_eagle eagle200_reader==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0870058a2c4..bfac71b9814 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ doorbirdpy==2.1.0 dsmr_parser==0.18 # homeassistant.components.dynalite -dynalite_devices==0.1.45 +dynalite_devices==0.1.46 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 From 04c849b0ee94fb2052d1f4d9cf95f754cdda91a5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 2 Sep 2020 12:38:53 -0400 Subject: [PATCH 577/862] Remove vizio test assertions for integration details in test_init (#39579) --- tests/components/vizio/test_init.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index 715800d006d..33764de8696 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -4,7 +4,6 @@ import pytest from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.components.vizio.const import DOMAIN from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_setup_component from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID @@ -39,11 +38,8 @@ async def test_load_and_unload( await hass.async_block_till_done() assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 assert DOMAIN in hass.data - assert "apps" in hass.data[DOMAIN] - assert isinstance(hass.data[DOMAIN]["apps"], DataUpdateCoordinator) assert await config_entry.async_unload(hass) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 - assert "apps" not in hass.data.get(DOMAIN, {}) assert DOMAIN not in hass.data From 7b3182fa8f3c66b26c9e291451a5b04a15f18aba Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Thu, 3 Sep 2020 00:42:12 +0800 Subject: [PATCH 578/862] Improve Yeelight code (#39543) * Rename ipaddr to ip_addr * Move custom services to entity services * Remove platform data * Change service setup to callback * Rename ip_addr to host * Use _host inside class --- homeassistant/components/yeelight/__init__.py | 63 +++-- .../components/yeelight/binary_sensor.py | 2 +- .../components/yeelight/config_flow.py | 26 +- homeassistant/components/yeelight/light.py | 255 ++++++++---------- .../components/yeelight/strings.json | 4 +- tests/components/yeelight/test_config_flow.py | 18 +- tests/components/yeelight/test_light.py | 10 +- 7 files changed, 173 insertions(+), 205 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index e463e5dad3f..f5403062faa 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -9,10 +9,9 @@ from yeelight import Bulb, BulbException, discover_bulbs from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_DEVICES, + CONF_HOST, CONF_ID, - CONF_IP_ADDRESS, CONF_NAME, CONF_SCAN_INTERVAL, ) @@ -126,8 +125,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -YEELIGHT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) - UPDATE_REQUEST_PROPERTIES = [ "power", "main_power", @@ -163,10 +160,10 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: } # Import manually configured devices - for ipaddr, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items(): - _LOGGER.debug("Importing configured %s", ipaddr) + for host, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items(): + _LOGGER.debug("Importing configured %s", host) entry_config = { - CONF_IP_ADDRESS: ipaddr, + CONF_HOST: host, **device_config, } hass.async_create_task( @@ -183,8 +180,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yeelight from a config entry.""" - async def _initialize(ipaddr: str) -> None: - device = await _async_setup_device(hass, ipaddr, entry.options) + async def _initialize(host: str) -> None: + device = await _async_setup_device(hass, host, entry.options) hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device for component in PLATFORMS: hass.async_create_task( @@ -197,7 +194,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_update_entry( entry, data={ - CONF_IP_ADDRESS: entry.data.get(CONF_IP_ADDRESS), + CONF_HOST: entry.data.get(CONF_HOST), CONF_ID: entry.data.get(CONF_ID), }, options={ @@ -218,9 +215,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNSUB_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener) } - if entry.data.get(CONF_IP_ADDRESS): + if entry.data.get(CONF_HOST): # manually added device - await _initialize(entry.data[CONF_IP_ADDRESS]) + await _initialize(entry.data[CONF_HOST]) else: # discovery scanner = YeelightScanner.async_get(hass) @@ -254,16 +251,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def _async_setup_device( hass: HomeAssistant, - ipaddr: str, + host: str, config: dict, ) -> None: # Set up device - bulb = Bulb(ipaddr, model=config.get(CONF_MODEL) or None) + bulb = Bulb(host, model=config.get(CONF_MODEL) or None) capabilities = await hass.async_add_executor_job(bulb.get_capabilities) if capabilities is None: # timeout - _LOGGER.error("Failed to get capabilities from %s", ipaddr) + _LOGGER.error("Failed to get capabilities from %s", host) raise ConfigEntryNotReady - device = YeelightDevice(hass, ipaddr, config, bulb) + device = YeelightDevice(hass, host, config, bulb) await hass.async_add_executor_job(device.update) await device.async_setup() return device @@ -303,11 +300,11 @@ class YeelightScanner: unique_id = device["capabilities"]["id"] if unique_id in self._seen: continue - ipaddr = device["ip"] - self._seen[unique_id] = ipaddr - _LOGGER.debug("Yeelight discovered at %s", ipaddr) + host = device["ip"] + self._seen[unique_id] = host + _LOGGER.debug("Yeelight discovered at %s", host) if unique_id in self._callbacks: - self._hass.async_create_task(self._callbacks[unique_id](ipaddr)) + self._hass.async_create_task(self._callbacks[unique_id](host)) self._callbacks.pop(unique_id) if len(self._callbacks) == 0: self._async_stop_scan() @@ -333,9 +330,9 @@ class YeelightScanner: @callback def async_register_callback(self, unique_id, callback_func): """Register callback function.""" - ipaddr = self._seen.get(unique_id) - if ipaddr is not None: - self._hass.async_add_job(callback_func(ipaddr)) + host = self._seen.get(unique_id) + if host is not None: + self._hass.async_add_job(callback_func(host)) else: self._callbacks[unique_id] = callback_func if len(self._callbacks) == 1: @@ -354,11 +351,11 @@ class YeelightScanner: class YeelightDevice: """Represents single Yeelight device.""" - def __init__(self, hass, ipaddr, config, bulb): + def __init__(self, hass, host, config, bulb): """Initialize device.""" self._hass = hass self._config = config - self._ipaddr = ipaddr + self._host = host unique_id = bulb.capabilities.get("id") self._name = config.get(CONF_NAME) or f"yeelight_{bulb.model}_{unique_id}" self._bulb_device = bulb @@ -382,9 +379,9 @@ class YeelightDevice: return self._config @property - def ipaddr(self): - """Return ip address.""" - return self._ipaddr + def host(self): + """Return hostname.""" + return self._host @property def available(self): @@ -472,7 +469,7 @@ class YeelightDevice: self.bulb.turn_off(duration=duration, light_type=light_type) except BulbException as ex: _LOGGER.error( - "Unable to turn the bulb off: %s, %s: %s", self.ipaddr, self.name, ex + "Unable to turn the bulb off: %s, %s: %s", self._host, self.name, ex ) def _update_properties(self): @@ -486,7 +483,7 @@ class YeelightDevice: except BulbException as ex: if self._available: # just inform once _LOGGER.error( - "Unable to update device %s, %s: %s", self.ipaddr, self.name, ex + "Unable to update device %s, %s: %s", self._host, self.name, ex ) self._available = False @@ -498,14 +495,14 @@ class YeelightDevice: self.bulb.get_capabilities() _LOGGER.debug( "Device %s, %s capabilities: %s", - self.ipaddr, + self._host, self.name, self.bulb.capabilities, ) except BulbException as ex: _LOGGER.error( "Unable to get device capabilities %s, %s: %s", - self.ipaddr, + self._host, self.name, ex, ) @@ -513,7 +510,7 @@ class YeelightDevice: def update(self): """Update device properties and send data updated signal.""" self._update_properties() - dispatcher_send(self._hass, DATA_UPDATED.format(self._ipaddr)) + dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) async def async_setup(self): """Set up the device.""" diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index ae811cd91d3..6d9de45c837 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -30,7 +30,7 @@ class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - DATA_UPDATED.format(self._device.ipaddr), + DATA_UPDATED.format(self._device.host), self.async_write_ha_state, ) ) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 656680c9c8b..84f1bbdd975 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -5,7 +5,7 @@ import voluptuous as vol import yeelight from homeassistant import config_entries, exceptions -from homeassistant.const import CONF_ID, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -45,9 +45,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: - if user_input.get(CONF_IP_ADDRESS): + if user_input.get(CONF_HOST): try: - await self._async_try_connect(user_input[CONF_IP_ADDRESS]) + await self._async_try_connect(user_input[CONF_HOST]) return self.async_create_entry( title=self._async_default_name(), data=user_input, @@ -61,7 +61,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Optional(CONF_IP_ADDRESS): str}), + data_schema=vol.Schema({vol.Optional(CONF_HOST): str}), errors=errors, ) @@ -90,8 +90,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if unique_id in configured_devices: continue # ignore configured devices model = capabilities["model"] - ipaddr = device["ip"] - name = f"{ipaddr} {model} {unique_id}" + host = device["ip"] + name = f"{host} {model} {unique_id}" self._discovered_devices[unique_id] = capabilities devices_name[unique_id] = name @@ -105,11 +105,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input=None): """Handle import step.""" - ipaddr = user_input[CONF_IP_ADDRESS] + host = user_input[CONF_HOST] try: - await self._async_try_connect(ipaddr) + await self._async_try_connect(host) except CannotConnect: - _LOGGER.error("Failed to import %s: cannot connect", ipaddr) + _LOGGER.error("Failed to import %s: cannot connect", host) return self.async_abort(reason="cannot_connect") except AlreadyConfigured: return self.async_abort(reason="already_configured") @@ -120,16 +120,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) - async def _async_try_connect(self, ipaddr): + async def _async_try_connect(self, host): """Set up with options.""" - bulb = yeelight.Bulb(ipaddr) + bulb = yeelight.Bulb(host) try: capabilities = await self.hass.async_add_executor_job(bulb.get_capabilities) if capabilities is None: # timeout - _LOGGER.error("Failed to get capabilities from %s: timeout", ipaddr) + _LOGGER.error("Failed to get capabilities from %s: timeout", host) raise CannotConnect except OSError as err: - _LOGGER.error("Failed to get capabilities from %s: %s", ipaddr, err) + _LOGGER.error("Failed to get capabilities from %s: %s", host, err) raise CannotConnect from err _LOGGER.debug("Get capabilities: %s", capabilities) self._capabilities = capabilities diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index cc580d60700..a84ebebf6e3 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -36,9 +36,9 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.service import extract_entity_ids import homeassistant.util.color as color_util from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -59,17 +59,13 @@ from . import ( DATA_CUSTOM_EFFECTS, DATA_DEVICE, DATA_UPDATED, - DATA_YEELIGHT, DOMAIN, YEELIGHT_FLOW_TRANSITION_SCHEMA, - YEELIGHT_SERVICE_SCHEMA, YeelightEntity, ) _LOGGER = logging.getLogger(__name__) -PLATFORM_DATA_KEY = f"{DATA_YEELIGHT}_lights" - SUPPORT_YEELIGHT = ( SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT ) @@ -148,59 +144,46 @@ EFFECTS_MAP = { VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) -SERVICE_SCHEMA_SET_MODE = YEELIGHT_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])} -) +SERVICE_SCHEMA_SET_MODE = { + vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode]) +} -SERVICE_SCHEMA_START_FLOW = YEELIGHT_SERVICE_SCHEMA.extend( - YEELIGHT_FLOW_TRANSITION_SCHEMA -) +SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA -SERVICE_SCHEMA_SET_COLOR_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_RGB_COLOR): vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) +SERVICE_SCHEMA_SET_COLOR_SCENE = { + vol.Required(ATTR_RGB_COLOR): vol.All( + vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + ), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, +} + +SERVICE_SCHEMA_SET_HSV_SCENE = { + vol.Required(ATTR_HS_COLOR): vol.All( + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=359)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) ), - vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, - } -) + vol.Coerce(tuple), + ), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, +} -SERVICE_SCHEMA_SET_HSV_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_HS_COLOR): vol.All( - vol.ExactSequence( - ( - vol.All(vol.Coerce(float), vol.Range(min=0, max=359)), - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), - ) - ), - vol.Coerce(tuple), - ), - vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, - } -) +SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE = { + vol.Required(ATTR_KELVIN): vol.All(vol.Coerce(int), vol.Range(min=1700, max=6500)), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, +} -SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_KELVIN): vol.All( - vol.Coerce(int), vol.Range(min=1700, max=6500) - ), - vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, - } -) +SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_FLOW_TRANSITION_SCHEMA -SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( - YEELIGHT_FLOW_TRANSITION_SCHEMA -) - -SERVICE_SCHEMA_SET_AUTO_DELAY_OFF = YEELIGHT_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), - vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, - } -) +SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE = { + vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, +} +@callback def _transitions_config_parser(transitions): """Parse transitions config into initialized objects.""" transition_objects = [] @@ -211,6 +194,7 @@ def _transitions_config_parser(transitions): return transition_objects +@callback def _parse_custom_effects(effects_config): effects = {} for config in effects_config: @@ -245,9 +229,6 @@ async def async_setup_entry( ) -> None: """Set up Yeelight from a config entry.""" - if PLATFORM_DATA_KEY not in hass.data: - hass.data[PLATFORM_DATA_KEY] = [] - custom_effects = _parse_custom_effects(hass.data[DOMAIN][DATA_CUSTOM_EFFECTS]) device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE] @@ -287,124 +268,114 @@ async def async_setup_entry( _lights_setup_helper(YeelightGenericLight) _LOGGER.warning( "Cannot determine device type for %s, %s. Falling back to white only", - device.ipaddr, + device.host, device.name, ) - hass.data[PLATFORM_DATA_KEY] += lights async_add_entities(lights, True) - await hass.async_add_executor_job(partial(setup_services, hass)) + _async_setup_services(hass) -def setup_services(hass): - """Set up the service listeners.""" +@callback +def _async_setup_services(hass: HomeAssistant): + """Set up custom services.""" - def service_call(func): - def service_to_entities(service): - """Return the known entities that a service call mentions.""" - - entity_ids = extract_entity_ids(hass, service) - target_devices = [ - light - for light in hass.data[PLATFORM_DATA_KEY] - if light.entity_id in entity_ids - ] - - return target_devices - - def service_to_params(service): - """Return service call params, without entity_id.""" - return { - key: value - for key, value in service.data.items() - if key != ATTR_ENTITY_ID - } - - def wrapper(service): - params = service_to_params(service) - target_devices = service_to_entities(service) - for device in target_devices: - func(device, params) - - return wrapper - - @service_call - def service_set_mode(target_device, params): - target_device.set_mode(**params) - - @service_call - def service_start_flow(target_devices, params): + async def _async_start_flow(entity, service_call): + params = {**service_call.data} + params.pop(ATTR_ENTITY_ID) params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS]) - target_devices.start_flow(**params) + await hass.async_add_executor_job(partial(entity.start_flow, **params)) - @service_call - def service_set_color_scene(target_device, params): - target_device.set_scene( - SceneClass.COLOR, *[*params[ATTR_RGB_COLOR], params[ATTR_BRIGHTNESS]] + async def _async_set_color_scene(entity, service_call): + await hass.async_add_executor_job( + partial( + entity.set_scene, + SceneClass.COLOR, + *service_call.data[ATTR_RGB_COLOR], + service_call.data[ATTR_BRIGHTNESS], + ) ) - @service_call - def service_set_hsv_scene(target_device, params): - target_device.set_scene( - SceneClass.HSV, *[*params[ATTR_HS_COLOR], params[ATTR_BRIGHTNESS]] + async def _async_set_hsv_scene(entity, service_call): + await hass.async_add_executor_job( + partial( + entity.set_scene, + SceneClass.HSV, + *service_call.data[ATTR_HS_COLOR], + service_call.data[ATTR_BRIGHTNESS], + ) ) - @service_call - def service_set_color_temp_scene(target_device, params): - target_device.set_scene( - SceneClass.CT, params[ATTR_KELVIN], params[ATTR_BRIGHTNESS] + async def _async_set_color_temp_scene(entity, service_call): + await hass.async_add_executor_job( + partial( + entity.set_scene, + SceneClass.CT, + service_call.data[ATTR_KELVIN], + service_call.data[ATTR_BRIGHTNESS], + ) ) - @service_call - def service_set_color_flow_scene(target_device, params): + async def _async_set_color_flow_scene(entity, service_call): flow = Flow( - count=params[ATTR_COUNT], - action=Flow.actions[params[ATTR_ACTION]], - transitions=_transitions_config_parser(params[ATTR_TRANSITIONS]), + count=service_call.data[ATTR_COUNT], + action=Flow.actions[service_call.data[ATTR_ACTION]], + transitions=_transitions_config_parser(service_call.data[ATTR_TRANSITIONS]), ) - target_device.set_scene(SceneClass.CF, flow) - - @service_call - def service_set_auto_delay_off_scene(target_device, params): - target_device.set_scene( - SceneClass.AUTO_DELAY_OFF, params[ATTR_BRIGHTNESS], params[ATTR_MINUTES] + await hass.async_add_executor_job( + partial( + entity.set_scene, + SceneClass.CF, + flow, + ) ) - hass.services.register( - DOMAIN, SERVICE_SET_MODE, service_set_mode, schema=SERVICE_SCHEMA_SET_MODE + async def _async_set_auto_delay_off_scene(entity, service_call): + await hass.async_add_executor_job( + partial( + entity.set_scene, + SceneClass.AUTO_DELAY_OFF, + service_call.data[ATTR_BRIGHTNESS], + service_call.data[ATTR_MINUTES], + ) + ) + + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_MODE, + SERVICE_SCHEMA_SET_MODE, + "set_mode", ) - hass.services.register( - DOMAIN, SERVICE_START_FLOW, service_start_flow, schema=SERVICE_SCHEMA_START_FLOW + platform.async_register_entity_service( + SERVICE_START_FLOW, + SERVICE_SCHEMA_START_FLOW, + _async_start_flow, ) - hass.services.register( - DOMAIN, + platform.async_register_entity_service( SERVICE_SET_COLOR_SCENE, - service_set_color_scene, - schema=SERVICE_SCHEMA_SET_COLOR_SCENE, + SERVICE_SCHEMA_SET_COLOR_SCENE, + _async_set_color_scene, ) - hass.services.register( - DOMAIN, + platform.async_register_entity_service( SERVICE_SET_HSV_SCENE, - service_set_hsv_scene, - schema=SERVICE_SCHEMA_SET_HSV_SCENE, + SERVICE_SCHEMA_SET_HSV_SCENE, + _async_set_hsv_scene, ) - hass.services.register( - DOMAIN, + platform.async_register_entity_service( SERVICE_SET_COLOR_TEMP_SCENE, - service_set_color_temp_scene, - schema=SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE, + SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE, + _async_set_color_temp_scene, ) - hass.services.register( - DOMAIN, + platform.async_register_entity_service( SERVICE_SET_COLOR_FLOW_SCENE, - service_set_color_flow_scene, - schema=SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE, + SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE, + _async_set_color_flow_scene, ) - hass.services.register( - DOMAIN, + platform.async_register_entity_service( SERVICE_SET_AUTO_DELAY_OFF_SCENE, - service_set_auto_delay_off_scene, - schema=SERVICE_SCHEMA_SET_AUTO_DELAY_OFF, + SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE, + _async_set_auto_delay_off_scene, ) @@ -442,7 +413,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - DATA_UPDATED.format(self._device.ipaddr), + DATA_UPDATED.format(self._device.host), self._schedule_immediate_update, ) ) diff --git a/homeassistant/components/yeelight/strings.json b/homeassistant/components/yeelight/strings.json index cc52b83f080..7fd3062ef87 100644 --- a/homeassistant/components/yeelight/strings.json +++ b/homeassistant/components/yeelight/strings.json @@ -3,9 +3,9 @@ "config": { "step": { "user": { - "description": "If you leave IP address empty, discovery will be used to find devices.", + "description": "If you leave the host empty, discovery will be used to find devices.", "data": { - "ip_address": "[%key:common::config_flow::data::ip%]" + "host": "[%key:common::config_flow::data::host%]" } }, "pick_device": { diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 7d1e51afbb1..921011a510d 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -16,7 +16,7 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_ID, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant from . import ( @@ -112,7 +112,7 @@ async def test_import(hass: HomeAssistant): """Test import from yaml.""" config = { CONF_NAME: DEFAULT_NAME, - CONF_IP_ADDRESS: IP_ADDRESS, + CONF_HOST: IP_ADDRESS, CONF_TRANSITION: DEFAULT_TRANSITION, CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, @@ -145,7 +145,7 @@ async def test_import(hass: HomeAssistant): assert result["title"] == DEFAULT_NAME assert result["data"] == { CONF_NAME: DEFAULT_NAME, - CONF_IP_ADDRESS: IP_ADDRESS, + CONF_HOST: IP_ADDRESS, CONF_TRANSITION: DEFAULT_TRANSITION, CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, @@ -178,7 +178,7 @@ async def test_manual(hass: HomeAssistant): mocked_bulb = _mocked_bulb(cannot_connect=True) with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_IP_ADDRESS: IP_ADDRESS} + result["flow_id"], {CONF_HOST: IP_ADDRESS} ) assert result2["type"] == "form" assert result2["step_id"] == "user" @@ -188,7 +188,7 @@ async def test_manual(hass: HomeAssistant): type(mocked_bulb).get_capabilities = MagicMock(side_effect=OSError) with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_IP_ADDRESS: IP_ADDRESS} + result["flow_id"], {CONF_HOST: IP_ADDRESS} ) assert result3["errors"] == {"base": "cannot_connect"} @@ -201,10 +201,10 @@ async def test_manual(hass: HomeAssistant): return_value=True, ): result4 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_IP_ADDRESS: IP_ADDRESS} + result["flow_id"], {CONF_HOST: IP_ADDRESS} ) assert result4["type"] == "create_entry" - assert result4["data"] == {CONF_IP_ADDRESS: IP_ADDRESS} + assert result4["data"] == {CONF_HOST: IP_ADDRESS} # Duplicate result = await hass.config_entries.flow.async_init( @@ -213,7 +213,7 @@ async def test_manual(hass: HomeAssistant): mocked_bulb = _mocked_bulb() with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_IP_ADDRESS: IP_ADDRESS} + result["flow_id"], {CONF_HOST: IP_ADDRESS} ) assert result2["type"] == "abort" assert result2["reason"] == "already_configured" @@ -221,7 +221,7 @@ async def test_manual(hass: HomeAssistant): async def test_options(hass: HomeAssistant): """Test options flow.""" - config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: IP_ADDRESS}) + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index aafe45851d3..8e8916ce303 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -71,7 +71,7 @@ from homeassistant.components.yeelight.light import ( YEELIGHT_MONO_EFFECT_LIST, YEELIGHT_TEMP_ONLY_EFFECT_LIST, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_ID, CONF_IP_ADDRESS, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.color import ( @@ -104,7 +104,7 @@ async def test_services(hass: HomeAssistant, caplog): domain=DOMAIN, data={ CONF_ID: "", - CONF_IP_ADDRESS: IP_ADDRESS, + CONF_HOST: IP_ADDRESS, CONF_TRANSITION: DEFAULT_TRANSITION, CONF_MODE_MUSIC: True, CONF_SAVE_ON_CHANGE: True, @@ -306,7 +306,7 @@ async def test_device_types(hass: HomeAssistant): domain=DOMAIN, data={ CONF_ID: "", - CONF_IP_ADDRESS: IP_ADDRESS, + CONF_HOST: IP_ADDRESS, CONF_TRANSITION: DEFAULT_TRANSITION, CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, @@ -337,7 +337,7 @@ async def test_device_types(hass: HomeAssistant): domain=DOMAIN, data={ CONF_ID: "", - CONF_IP_ADDRESS: IP_ADDRESS, + CONF_HOST: IP_ADDRESS, CONF_TRANSITION: DEFAULT_TRANSITION, CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, @@ -520,7 +520,7 @@ async def test_effects(hass: HomeAssistant): domain=DOMAIN, data={ CONF_ID: "", - CONF_IP_ADDRESS: IP_ADDRESS, + CONF_HOST: IP_ADDRESS, CONF_TRANSITION: DEFAULT_TRANSITION, CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, From 5fafaa3c4f7572dde7fe356049d488af39d48fda Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 2 Sep 2020 12:56:09 -0400 Subject: [PATCH 579/862] Add get_migration_config to zwave websocket api (#39577) * Add get_migration_config to zwave websocket api * Add test --- homeassistant/components/zwave/websocket_api.py | 16 ++++++++++++++++ tests/components/zwave/test_websocket_api.py | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/components/zwave/websocket_api.py b/homeassistant/components/zwave/websocket_api.py index 7454f2e2c6a..24bf8d80a75 100644 --- a/homeassistant/components/zwave/websocket_api.py +++ b/homeassistant/components/zwave/websocket_api.py @@ -10,6 +10,7 @@ from homeassistant.core import callback from .const import ( CONF_AUTOHEAL, CONF_DEBUG, + CONF_NETWORK_KEY, CONF_POLLING_INTERVAL, CONF_USB_STICK_PATH, DATA_NETWORK, @@ -46,8 +47,23 @@ def websocket_get_config(hass, connection, msg): ) +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required(TYPE): "zwave/get_migration_config"}) +def websocket_get_migration_config(hass, connection, msg): + """Get Z-Wave configuration for migration.""" + config = hass.data[DATA_ZWAVE_CONFIG] + connection.send_result( + msg[ID], + { + CONF_USB_STICK_PATH: config[CONF_USB_STICK_PATH], + CONF_NETWORK_KEY: config[CONF_NETWORK_KEY], + }, + ) + + @callback def async_load_websocket_api(hass): """Set up the web socket API.""" websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_get_config) + websocket_api.async_register_command(hass, websocket_get_migration_config) diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py index 978fc09a10d..25bc364a630 100644 --- a/tests/components/zwave/test_websocket_api.py +++ b/tests/components/zwave/test_websocket_api.py @@ -2,6 +2,7 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components.zwave.const import ( CONF_AUTOHEAL, + CONF_NETWORK_KEY, CONF_POLLING_INTERVAL, CONF_USB_STICK_PATH, ) @@ -19,6 +20,7 @@ async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): CONF_AUTOHEAL: False, CONF_USB_STICK_PATH: "/dev/zwave", CONF_POLLING_INTERVAL: 6000, + CONF_NETWORK_KEY: "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST", } }, ) @@ -35,3 +37,13 @@ async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): assert result[CONF_USB_STICK_PATH] == "/dev/zwave" assert not result[CONF_AUTOHEAL] assert result[CONF_POLLING_INTERVAL] == 6000 + + await client.send_json({ID: 6, TYPE: "zwave/get_migration_config"}) + msg = await client.receive_json() + result = msg["result"] + + assert result[CONF_USB_STICK_PATH] == "/dev/zwave" + assert ( + result[CONF_NETWORK_KEY] + == "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST" + ) From e3354895f859ed75eb3c05f8c56a075ac262b0e7 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 2 Sep 2020 19:59:21 +0200 Subject: [PATCH 580/862] Round sensor readings for bom (#39513) --- homeassistant/components/bom/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 59ee8027180..a32b36796de 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -231,7 +231,11 @@ class BOMCurrentData: through the entire BOM provided dataset. """ condition_readings = (entry[condition] for entry in self._data) - return next((x for x in condition_readings if x != "-"), None) + reading = next((x for x in condition_readings if x != "-"), None) + + if isinstance(reading, (int, float)): + return round(reading, 2) + return reading def should_update(self): """Determine whether an update should occur. From 97602a127adc0ef138842212f9bea956e83786cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 2 Sep 2020 20:56:23 +0200 Subject: [PATCH 581/862] Reintroduce custom met.no url (#39583) --- homeassistant/components/met/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 4eedfc0b76d..5a357467920 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -22,6 +22,9 @@ import homeassistant.util.dt as dt_util from .const import CONF_TRACK_HOME, DOMAIN +URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" + + _LOGGER = logging.getLogger(__name__) @@ -139,7 +142,7 @@ class MetWeatherData: } self._weather_data = metno.MetWeatherData( - coordinates, async_get_clientsession(self.hass) + coordinates, async_get_clientsession(self.hass), api_url=URL ) async def fetch_data(self): From cb1cf2238dd49bc7fad09aa88a97536206ee45c4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 2 Sep 2020 13:56:41 -0500 Subject: [PATCH 582/862] Add Plex service to refresh a library (#39094) * Add Plex service to refresh a library * Clean up rebase leftovers * Re-run black * Fix docstring Co-authored-by: Charles Garwood Co-authored-by: Charles Garwood --- homeassistant/components/plex/__init__.py | 3 + homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/services.py | 75 ++++++++++++++ homeassistant/components/plex/services.yaml | 10 ++ tests/components/plex/mock_classes.py | 6 +- tests/components/plex/test_services.py | 102 ++++++++++++++++++++ 6 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/plex/services.py create mode 100644 tests/components/plex/test_services.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 3552823677d..15212175853 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -43,6 +43,7 @@ from .const import ( ) from .errors import ShouldUpdateConfigEntry from .server import PlexServer +from .services import async_setup_services _LOGGER = logging.getLogger(__package__) @@ -54,6 +55,8 @@ async def async_setup(hass, config): {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}}, ) + await async_setup_services(hass) + return True diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index e8077a00983..b914a89f744 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -45,3 +45,4 @@ AUTOMATIC_SETUP_STRING = "Obtain a new token from plex.tv" MANUAL_SETUP_STRING = "Configure Plex server manually" SERVICE_PLAY_ON_SONOS = "play_on_sonos" +SERVICE_REFRESH_LIBRARY = "refresh_library" diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py new file mode 100644 index 00000000000..cd127c1465e --- /dev/null +++ b/homeassistant/components/plex/services.py @@ -0,0 +1,75 @@ +"""Services for the Plex integration.""" +import logging + +from plexapi.exceptions import NotFound +import voluptuous as vol + +from .const import DOMAIN, SERVERS, SERVICE_REFRESH_LIBRARY + +REFRESH_LIBRARY_SCHEMA = vol.Schema( + {vol.Optional("server_name"): str, vol.Required("library_name"): str} +) + +_LOGGER = logging.getLogger(__package__) + + +async def async_setup_services(hass): + """Set up services for the Plex component.""" + + async def async_refresh_library_service(service_call): + await hass.async_add_executor_job(refresh_library, hass, service_call) + + hass.services.async_register( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + async_refresh_library_service, + schema=REFRESH_LIBRARY_SCHEMA, + ) + + return True + + +def refresh_library(hass, service_call): + """Scan a Plex library for new and updated media.""" + plex_server_name = service_call.data.get("server_name") + library_name = service_call.data.get("library_name") + + plex_server = get_plex_server(hass, plex_server_name) + if not plex_server: + return + + try: + library = plex_server.library.section(title=library_name) + except NotFound: + _LOGGER.error( + "Library with name '%s' not found in %s", + library_name, + list(map(lambda x: x.title, plex_server.library.sections())), + ) + return + + _LOGGER.info("Scanning %s for new and updated media", library_name) + library.update() + + +def get_plex_server(hass, plex_server_name=None): + """Retrieve a configured Plex server by name.""" + plex_servers = hass.data[DOMAIN][SERVERS].values() + + if plex_server_name: + plex_server = [x for x in plex_servers if x.friendly_name == plex_server_name] + if not plex_server: + _LOGGER.error( + "Requested Plex server '%s' not found in %s", + plex_server_name, + list(map(lambda x: x.friendly_name, plex_servers)), + ) + return None + elif len(plex_servers) == 1: + return next(iter(plex_servers)) + + _LOGGER.error( + "Multiple Plex servers configured and no selection made: %s", + list(map(lambda x: x.friendly_name, plex_servers)), + ) + return None diff --git a/homeassistant/components/plex/services.yaml b/homeassistant/components/plex/services.yaml index 0245edfb99e..e9fb40939b3 100644 --- a/homeassistant/components/plex/services.yaml +++ b/homeassistant/components/plex/services.yaml @@ -11,3 +11,13 @@ play_on_sonos: media_content_type: description: The type of content to play. Must be "music". example: "music" + +refresh_library: + description: Refresh a Plex library to scan for new and updated media. + fields: + server_name: + description: Name of a Plex server if multiple Plex servers configured. + example: "My Plex Server" + library_name: + description: Name of the Plex library to refresh. + example: "TV Shows" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index f05c1017023..287712cf520 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -121,8 +121,6 @@ class MockPlexServer: self._systemAccounts = list(map(MockPlexSystemAccount, range(num_users))) - self._library = None - self._clients = [] self._sessions = [] self.set_clients(num_users) @@ -400,6 +398,10 @@ class MockPlexLibrarySection: """Mock the key identifier property.""" return str(id(self.title)) + def update(self): + """Mock the update call.""" + pass + class MockPlexMediaItem: """Mock a Plex Media instance.""" diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py new file mode 100644 index 00000000000..b40c9f6d4d3 --- /dev/null +++ b/tests/components/plex/test_services.py @@ -0,0 +1,102 @@ +"""Tests for various Plex services.""" +from homeassistant.components.plex.const import ( + CONF_SERVER, + CONF_SERVER_IDENTIFIER, + DOMAIN, + PLEX_SERVER_CONFIG, + SERVICE_REFRESH_LIBRARY, +) +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_TOKEN, + CONF_URL, + CONF_VERIFY_SSL, +) + +from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN +from .mock_classes import MockPlexAccount, MockPlexLibrarySection, MockPlexServer + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_refresh_library(hass): + """Test refresh_library service call.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), 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() + + # Test with non-existent server + with patch.object(MockPlexLibrarySection, "update") as mock_update: + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"server_name": "Not a Server", "library_name": "Movies"}, + True, + ) + assert not mock_update.called + + # Test with non-existent library + with patch.object(MockPlexLibrarySection, "update") as mock_update: + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"library_name": "Not a Library"}, + True, + ) + assert not mock_update.called + + # Test with valid library + with patch.object(MockPlexLibrarySection, "update") as mock_update: + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"library_name": "Movies"}, + True, + ) + assert mock_update.called + + # Add a second configured server + entry_2 = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_SERVER: MOCK_SERVERS[1][CONF_SERVER], + PLEX_SERVER_CONFIG: { + CONF_TOKEN: MOCK_TOKEN, + CONF_URL: f"https://{MOCK_SERVERS[1][CONF_HOST]}:{MOCK_SERVERS[1][CONF_PORT]}", + CONF_VERIFY_SSL: True, + }, + CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][CONF_SERVER_IDENTIFIER], + }, + ) + + mock_plex_server_2 = MockPlexServer(config_entry=entry_2) + with patch("plexapi.server.PlexServer", return_value=mock_plex_server_2), patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True): + entry_2.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry_2.entry_id) + await hass.async_block_till_done() + + # Test multiple servers available but none specified + with patch.object(MockPlexLibrarySection, "update") as mock_update: + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"library_name": "Movies"}, + True, + ) + assert not mock_update.called From 2d2efeb9bb507dd2e8bc4225042d9babff96cb7d Mon Sep 17 00:00:00 2001 From: Tomasz Date: Wed, 2 Sep 2020 21:28:18 +0200 Subject: [PATCH 583/862] Add the ability to reload rpi_gpio platforms from yaml (#39548) * add reload service * test for reload service * missing file * Revert "missing file" This reverts commit 24391fe3b9d9a5c03da8249422b394634a633bb2. * Revert "test for reload service" This reverts commit 5bda48d07000e533298766b5c3acb90e0f91fb50. --- homeassistant/components/rpi_gpio/__init__.py | 1 + homeassistant/components/rpi_gpio/binary_sensor.py | 6 ++++++ homeassistant/components/rpi_gpio/cover.py | 6 ++++++ homeassistant/components/rpi_gpio/services.yaml | 2 ++ homeassistant/components/rpi_gpio/switch.py | 6 ++++++ 5 files changed, 21 insertions(+) create mode 100644 homeassistant/components/rpi_gpio/services.yaml diff --git a/homeassistant/components/rpi_gpio/__init__.py b/homeassistant/components/rpi_gpio/__init__.py index b2c3cf6b7bb..b3a83646f0e 100644 --- a/homeassistant/components/rpi_gpio/__init__.py +++ b/homeassistant/components/rpi_gpio/__init__.py @@ -8,6 +8,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_S _LOGGER = logging.getLogger(__name__) DOMAIN = "rpi_gpio" +PLATFORMS = ["binary_sensor", "cover", "switch"] def setup(hass, config): diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py index a7ecaa3d36c..527da2a7096 100644 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -7,6 +7,9 @@ from homeassistant.components import rpi_gpio from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service + +from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -33,6 +36,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Raspberry PI GPIO devices.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + pull_mode = config.get(CONF_PULL_MODE) bouncetime = config.get(CONF_BOUNCETIME) invert_logic = config.get(CONF_INVERT_LOGIC) diff --git a/homeassistant/components/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py index 56e76959ecc..fab35da31fb 100644 --- a/homeassistant/components/rpi_gpio/cover.py +++ b/homeassistant/components/rpi_gpio/cover.py @@ -8,6 +8,9 @@ from homeassistant.components import rpi_gpio from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service + +from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -49,6 +52,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RPi cover platform.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + relay_time = config.get(CONF_RELAY_TIME) state_pull_mode = config.get(CONF_STATE_PULL_MODE) invert_state = config.get(CONF_INVERT_STATE) diff --git a/homeassistant/components/rpi_gpio/services.yaml b/homeassistant/components/rpi_gpio/services.yaml new file mode 100644 index 00000000000..d0564941cdb --- /dev/null +++ b/homeassistant/components/rpi_gpio/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all rpi_gpio entities. diff --git a/homeassistant/components/rpi_gpio/switch.py b/homeassistant/components/rpi_gpio/switch.py index 03cb9f083ce..047bff39c95 100644 --- a/homeassistant/components/rpi_gpio/switch.py +++ b/homeassistant/components/rpi_gpio/switch.py @@ -8,6 +8,9 @@ from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.reload import setup_reload_service + +from . import DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -29,6 +32,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Raspberry PI GPIO devices.""" + + setup_reload_service(hass, DOMAIN, PLATFORMS) + invert_logic = config.get(CONF_INVERT_LOGIC) switches = [] From a778690b6438429b63f985ebbec4f1559525f27d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2020 17:12:07 -0500 Subject: [PATCH 584/862] Support reloading the group notify platform (#39511) --- homeassistant/components/group/__init__.py | 2 +- homeassistant/components/group/services.yaml | 2 +- homeassistant/components/notify/__init__.py | 251 +++++++++++-------- homeassistant/helpers/reload.py | 102 ++++++-- tests/components/group/test_light.py | 74 ++++++ tests/components/group/test_notify.py | 60 ++++- tests/fixtures/group/configuration.yaml | 7 + tests/helpers/test_reload.py | 101 +++++++- 8 files changed, 470 insertions(+), 129 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index cda49591d5c..87eb2cd615b 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -58,7 +58,7 @@ ATTR_ALL = "all" SERVICE_SET = "set" SERVICE_REMOVE = "remove" -PLATFORMS = ["light", "cover"] +PLATFORMS = ["light", "cover", "notify"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index cec4f187ca6..57e11d672dc 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -1,6 +1,6 @@ # Describes the format for available group services reload: - description: Reload group configuration. + description: Reload group configuration, entities, and notify services. set: description: Create/Update a user group. diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 77ec48c5435..9a4ec681aab 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -2,11 +2,12 @@ import asyncio from functools import partial import logging -from typing import Optional +from typing import Any, Dict, Optional import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.core import ServiceCall from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv @@ -37,10 +38,6 @@ DOMAIN = "notify" SERVICE_NOTIFY = "notify" NOTIFY_SERVICES = "notify_services" -SERVICE = "service" -TARGETS = "targets" -FRIENDLY_NAME = "friendly_name" -TARGET_FRIENDLY_NAME = "target_friendly_name" PLATFORM_SCHEMA = vol.Schema( {vol.Required(CONF_PLATFORM): cv.string, vol.Optional(CONF_NAME): cv.string}, @@ -58,88 +55,160 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema( @bind_hass -async def async_reload(hass, integration_name): +async def async_reload(hass: HomeAssistantType, integration_name: str) -> None: """Register notify services for an integration.""" + if not _async_integration_has_notify_services(hass, integration_name): + return + + tasks = [ + notify_service.async_register_services() + for notify_service in hass.data[NOTIFY_SERVICES][integration_name] + ] + + await asyncio.gather(*tasks) + + +@bind_hass +async def async_reset_platform(hass: HomeAssistantType, integration_name: str) -> None: + """Unregister notify services for an integration.""" + if not _async_integration_has_notify_services(hass, integration_name): + return + + tasks = [ + notify_service.async_unregister_services() + for notify_service in hass.data[NOTIFY_SERVICES][integration_name] + ] + + await asyncio.gather(*tasks) + + del hass.data[NOTIFY_SERVICES][integration_name] + + +def _async_integration_has_notify_services( + hass: HomeAssistantType, integration_name: str +) -> bool: + """Determine if an integration has notify services registered.""" if ( NOTIFY_SERVICES not in hass.data or integration_name not in hass.data[NOTIFY_SERVICES] ): - return + return False - tasks = [ - _async_setup_notify_services(hass, data) - for data in hass.data[NOTIFY_SERVICES][integration_name] - ] - await asyncio.gather(*tasks) + return True -async def _async_setup_notify_services(hass, data): - """Create or remove the notify services.""" - notify_service = data[SERVICE] - friendly_name = data[FRIENDLY_NAME] - targets = data[TARGETS] +class BaseNotificationService: + """An abstract class for notification services.""" - async def _async_notify_message(service): + hass: Optional[HomeAssistantType] = None + + def send_message(self, message, **kwargs): + """Send a message. + + kwargs can contain ATTR_TITLE to specify a title. + """ + raise NotImplementedError() + + async def async_send_message(self, message: Any, **kwargs: Any) -> None: + """Send a message. + + kwargs can contain ATTR_TITLE to specify a title. + """ + await self.hass.async_add_job(partial(self.send_message, message, **kwargs)) # type: ignore + + async def _async_notify_message_service(self, service: ServiceCall) -> None: """Handle sending notification message service calls.""" - await _async_notify_message_service(hass, service, notify_service, targets) + kwargs = {} + message = service.data[ATTR_MESSAGE] + title = service.data.get(ATTR_TITLE) - if hasattr(notify_service, "targets"): - target_friendly_name = data[TARGET_FRIENDLY_NAME] - stale_targets = set(targets) + if title: + title.hass = self.hass + kwargs[ATTR_TITLE] = title.async_render() - for name, target in notify_service.targets.items(): - target_name = slugify(f"{target_friendly_name}_{name}") - if target_name in stale_targets: - stale_targets.remove(target_name) - if target_name in targets: - continue - targets[target_name] = target - hass.services.async_register( - DOMAIN, - target_name, - _async_notify_message, - schema=NOTIFY_SERVICE_SCHEMA, - ) + if self._registered_targets.get(service.service) is not None: + kwargs[ATTR_TARGET] = [self._registered_targets[service.service]] + elif service.data.get(ATTR_TARGET) is not None: + kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) - for stale_target_name in stale_targets: - del targets[stale_target_name] - hass.services.async_remove( - DOMAIN, - stale_target_name, - ) + message.hass = self.hass + kwargs[ATTR_MESSAGE] = message.async_render() + kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) - friendly_name_slug = slugify(friendly_name) - if hass.services.has_service(DOMAIN, friendly_name_slug): - return + await self.async_send_message(**kwargs) - hass.services.async_register( - DOMAIN, - friendly_name_slug, - _async_notify_message, - schema=NOTIFY_SERVICE_SCHEMA, - ) + async def async_setup( + self, + hass: HomeAssistantType, + service_name: str, + target_service_name_prefix: str, + ) -> None: + """Store the data for the notify service.""" + # pylint: disable=attribute-defined-outside-init + self.hass = hass + self._service_name = service_name + self._target_service_name_prefix = target_service_name_prefix + self._registered_targets: Dict = {} + async def async_register_services(self) -> None: + """Create or update the notify services.""" + assert self.hass -async def _async_notify_message_service(hass, service, notify_service, targets): - """Handle sending notification message service calls.""" - kwargs = {} - message = service.data[ATTR_MESSAGE] - title = service.data.get(ATTR_TITLE) + if hasattr(self, "targets"): + stale_targets = set(self._registered_targets) - if title: - title.hass = hass - kwargs[ATTR_TITLE] = title.async_render() + # pylint: disable=no-member + for name, target in self.targets.items(): # type: ignore + target_name = slugify(f"{self._target_service_name_prefix}_{name}") + if target_name in stale_targets: + stale_targets.remove(target_name) + if target_name in self._registered_targets: + continue + self._registered_targets[target_name] = target + self.hass.services.async_register( + DOMAIN, + target_name, + self._async_notify_message_service, + schema=NOTIFY_SERVICE_SCHEMA, + ) - if targets.get(service.service) is not None: - kwargs[ATTR_TARGET] = [targets[service.service]] - elif service.data.get(ATTR_TARGET) is not None: - kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) + for stale_target_name in stale_targets: + del self._registered_targets[stale_target_name] + self.hass.services.async_remove( + DOMAIN, + stale_target_name, + ) - message.hass = hass - kwargs[ATTR_MESSAGE] = message.async_render() - kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) + if self.hass.services.has_service(DOMAIN, self._service_name): + return - await notify_service.async_send_message(**kwargs) + self.hass.services.async_register( + DOMAIN, + self._service_name, + self._async_notify_message_service, + schema=NOTIFY_SERVICE_SCHEMA, + ) + + async def async_unregister_services(self) -> None: + """Unregister the notify services.""" + assert self.hass + + if self._registered_targets: + remove_targets = set(self._registered_targets) + for remove_target_name in remove_targets: + del self._registered_targets[remove_target_name] + self.hass.services.async_remove( + DOMAIN, + remove_target_name, + ) + + if not self.hass.services.has_service(DOMAIN, self._service_name): + return + + self.hass.services.async_remove( + DOMAIN, + self._service_name, + ) async def async_setup(hass, config): @@ -188,31 +257,19 @@ async def async_setup(hass, config): _LOGGER.exception("Error setting up platform %s", integration_name) return - notify_service.hass = hass - if discovery_info is None: discovery_info = {} - target_friendly_name = ( - p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or integration_name + conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) + target_service_name_prefix = conf_name or integration_name + service_name = slugify(conf_name or SERVICE_NOTIFY) + + await notify_service.async_setup(hass, service_name, target_service_name_prefix) + await notify_service.async_register_services() + + hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append( + notify_service ) - friendly_name = ( - p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or SERVICE_NOTIFY - ) - - data = { - FRIENDLY_NAME: friendly_name, - # The targets use a slightly different friendly name - # selection pattern than the base service - TARGET_FRIENDLY_NAME: target_friendly_name, - SERVICE: notify_service, - TARGETS: {}, - } - hass.data[NOTIFY_SERVICES].setdefault(integration_name, []) - hass.data[NOTIFY_SERVICES][integration_name].append(data) - - await _async_setup_notify_services(hass, data) - hass.config.components.add(f"{DOMAIN}.{integration_name}") return True @@ -232,23 +289,3 @@ async def async_setup(hass, config): discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) return True - - -class BaseNotificationService: - """An abstract class for notification services.""" - - hass: Optional[HomeAssistantType] = None - - def send_message(self, message, **kwargs): - """Send a message. - - kwargs can contain ATTR_TITLE to specify a title. - """ - raise NotImplementedError() - - async def async_send_message(self, message, **kwargs): - """Send a message. - - kwargs can contain ATTR_TITLE to specify a title. - """ - await self.hass.async_add_job(partial(self.send_message, message, **kwargs)) diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 1ffba25ce15..19a1e46a6d4 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -2,7 +2,7 @@ import asyncio import logging -from typing import Any, Dict, Iterable, Optional +from typing import Any, Dict, Iterable, List, Optional from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD @@ -12,6 +12,7 @@ from homeassistant.helpers import config_per_platform from homeassistant.helpers.entity_platform import DATA_ENTITY_PLATFORM, EntityPlatform from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration +from homeassistant.setup import async_setup_component _LOGGER = logging.getLogger(__name__) @@ -34,29 +35,94 @@ async def async_reload_integration_platforms( _LOGGER.error(err) return - for integration_platform in integration_platforms: - platform = async_get_platform(hass, integration_name, integration_platform) - - if not platform: - continue - - integration = await async_get_integration(hass, integration_platform) - - conf = await conf_util.async_process_component_config( - hass, unprocessed_conf, integration + tasks = [ + _resetup_platform( + hass, integration_name, integration_platform, unprocessed_conf ) + for integration_platform in integration_platforms + ] - if not conf: + await asyncio.gather(*tasks) + + +async def _resetup_platform( + hass: HomeAssistantType, + integration_name: str, + integration_platform: str, + unprocessed_conf: Dict, +) -> None: + """Resetup a platform.""" + integration = await async_get_integration(hass, integration_platform) + + conf = await conf_util.async_process_component_config( + hass, unprocessed_conf, integration + ) + + if not conf: + return + + root_config: Dict = {integration_platform: []} + # Extract only the config for template, ignore the rest. + for p_type, p_config in config_per_platform(conf, integration_platform): + if p_type != integration_name: continue - await platform.async_reset() + root_config[integration_platform].append(p_config) - # Extract only the config for template, ignore the rest. - for p_type, p_config in config_per_platform(conf, integration_platform): - if p_type != integration_name: - continue + component = integration.get_component() - await platform.async_setup(p_config) # type: ignore + if hasattr(component, "async_reset_platform"): + # If the integration has its own way to reset + # use this method. + await component.async_reset_platform(hass, integration_name) # type: ignore + await component.async_setup(hass, root_config) # type: ignore + return + + # If its an entity platform, we use the entity_platform + # async_reset method + platform = async_get_platform(hass, integration_name, integration_platform) + if platform: + await _async_reconfig_platform(platform, root_config[integration_platform]) + return + + if not root_config[integration_platform]: + # No config for this platform + # and its not loaded. Nothing to do + return + + await _async_setup_platform( + hass, integration_name, integration_platform, root_config[integration_platform] + ) + + +async def _async_setup_platform( + hass: HomeAssistantType, + integration_name: str, + integration_platform: str, + platform_configs: List[Dict], +) -> None: + """Platform for the first time when new configuration is added.""" + if integration_platform not in hass.data: + await async_setup_component( + hass, integration_platform, {integration_platform: platform_configs} + ) + return + + entity_component = hass.data[integration_platform] + tasks = [ + entity_component.async_setup_platform(integration_name, p_config) + for p_config in platform_configs + ] + await asyncio.gather(*tasks) + + +async def _async_reconfig_platform( + platform: EntityPlatform, platform_configs: List[Dict] +) -> None: + """Reconfigure an already loaded platform.""" + await platform.async_reset() + tasks = [platform.async_setup(p_config) for p_config in platform_configs] # type: ignore + await asyncio.gather(*tasks) async def async_integration_yaml_config( diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 8855ca97626..a22c56b4bfc 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -683,5 +683,79 @@ async def test_reload(hass): assert hass.states.get("light.outside_patio_lights_g") is not None +async def test_reload_with_platform_not_setup(hass): + """Test the ability to reload lights.""" + hass.states.async_set("light.bowl", STATE_ON) + await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "demo"}, + ] + }, + ) + assert await async_setup_component( + hass, + "group", + { + "group": { + "group_zero": {"entities": "light.Bowl", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "group/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("light.light_group") is None + assert hass.states.get("light.master_hall_lights_g") is not None + assert hass.states.get("light.outside_patio_lights_g") is not None + + +async def test_reload_with_base_integration_platform_not_setup(hass): + """Test the ability to reload lights.""" + assert await async_setup_component( + hass, + "group", + { + "group": { + "group_zero": {"entities": "light.Bowl", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "group/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("light.light_group") is None + assert hass.states.get("light.master_hall_lights_g") is not None + assert hass.states.get("light.outside_patio_lights_g") is not None + + def _get_fixtures_base_path(): return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index b120cf2cea4..62b395dcf4d 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,11 +1,14 @@ """The tests for the notify.group platform.""" import asyncio +from os import path import unittest +from homeassistant import config as hass_config import homeassistant.components.demo.notify as demo +from homeassistant.components.group import SERVICE_RELOAD import homeassistant.components.group.notify as group import homeassistant.components.notify as notify -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component, setup_component from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component, get_test_home_assistant @@ -90,3 +93,58 @@ class TestNotifyGroup(unittest.TestCase): "title": "Test notification", "data": {"hello": "world", "test": "message"}, } + + +async def test_reload_notify(hass): + """Verify we can reload the notify service.""" + + assert await async_setup_component( + hass, + "group", + {}, + ) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + notify.DOMAIN, + { + notify.DOMAIN: [ + {"name": "demo1", "platform": "demo"}, + {"name": "demo2", "platform": "demo"}, + { + "name": "group_notify", + "platform": "group", + "services": [{"service": "demo1"}], + }, + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(notify.DOMAIN, "demo1") + assert hass.services.has_service(notify.DOMAIN, "demo2") + assert hass.services.has_service(notify.DOMAIN, "group_notify") + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "group/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "group", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(notify.DOMAIN, "demo1") + assert hass.services.has_service(notify.DOMAIN, "demo2") + assert not hass.services.has_service(notify.DOMAIN, "group_notify") + assert hass.services.has_service(notify.DOMAIN, "new_group_notify") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/group/configuration.yaml b/tests/fixtures/group/configuration.yaml index 9047024e3de..0a5c9e18bd1 100644 --- a/tests/fixtures/group/configuration.yaml +++ b/tests/fixtures/group/configuration.yaml @@ -9,3 +9,10 @@ light: entities: - light.outside_patio_lights - light.outside_patio_lights_2 + +notify: + - platform: group + name: new_group_notify + services: + - service: demo1 + - service: demo2 diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index dafcbebdb6e..25844151533 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -13,8 +13,9 @@ from homeassistant.helpers.reload import ( async_reload_integration_platforms, async_setup_reload_service, ) +from homeassistant.loader import async_get_integration -from tests.async_mock import Mock, patch +from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockModule, MockPlatform, @@ -109,6 +110,104 @@ async def test_setup_reload_service(hass): assert len(setup_called) == 2 +async def test_setup_reload_service_when_async_process_component_config_fails(hass): + """Test setting up a reload service with the config processing failing.""" + component_setup = Mock(return_value=True) + + setup_called = [] + + async def setup_platform(*args): + setup_called.append(args) + + mock_integration(hass, MockModule(DOMAIN, setup=component_setup)) + mock_integration(hass, MockModule(PLATFORM, dependencies=[DOMAIN])) + + mock_platform = MockPlatform(async_setup_platform=setup_platform) + mock_entity_platform(hass, f"{DOMAIN}.{PLATFORM}", mock_platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_setup({DOMAIN: {"platform": PLATFORM, "sensors": None}}) + await hass.async_block_till_done() + assert component_setup.called + + assert f"{DOMAIN}.{PLATFORM}" in hass.config.components + assert len(setup_called) == 1 + + await async_setup_reload_service(hass, PLATFORM, [DOMAIN]) + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "helpers/reload_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path), patch.object( + config, "async_process_component_config", return_value=None + ): + await hass.services.async_call( + PLATFORM, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(setup_called) == 1 + + +async def test_setup_reload_service_with_platform_that_provides_async_reset_platform( + hass, +): + """Test setting up a reload service using a platform that has its own async_reset_platform.""" + component_setup = AsyncMock(return_value=True) + + setup_called = [] + async_reset_platform_called = [] + + async def setup_platform(*args): + setup_called.append(args) + + async def async_reset_platform(*args): + async_reset_platform_called.append(args) + + mock_integration(hass, MockModule(DOMAIN, async_setup=component_setup)) + integration = await async_get_integration(hass, DOMAIN) + integration.get_component().async_reset_platform = async_reset_platform + + mock_integration(hass, MockModule(PLATFORM, dependencies=[DOMAIN])) + + mock_platform = MockPlatform(async_setup_platform=setup_platform) + mock_entity_platform(hass, f"{DOMAIN}.{PLATFORM}", mock_platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_setup({DOMAIN: {"platform": PLATFORM, "name": "xyz"}}) + await hass.async_block_till_done() + assert component_setup.called + + assert f"{DOMAIN}.{PLATFORM}" in hass.config.components + assert len(setup_called) == 1 + + await async_setup_reload_service(hass, PLATFORM, [DOMAIN]) + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "helpers/reload_configuration.yaml", + ) + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + PLATFORM, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(setup_called) == 1 + assert len(async_reset_platform_called) == 1 + + async def test_async_integration_yaml_config(hass): """Test loading yaml config for an integration.""" mock_integration(hass, MockModule(DOMAIN)) From 661b593db3bfa101f0d7f7a4c15d28dc4a0d6e9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2020 18:25:43 -0500 Subject: [PATCH 585/862] Support reloading the rest notify platform (#39527) * Support reloading the rest notify platform * update services.yaml * fix conflict --- homeassistant/components/rest/__init__.py | 2 +- homeassistant/components/rest/notify.py | 5 ++ homeassistant/components/rest/services.yaml | 2 +- homeassistant/helpers/reload.py | 6 +++ tests/components/rest/test_notify.py | 52 +++++++++++++++++++++ tests/fixtures/rest/configuration.yaml | 5 ++ 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/components/rest/test_notify.py diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index e0b743c1d37..69bc6172341 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -1,4 +1,4 @@ """The rest component.""" DOMAIN = "rest" -PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "notify", "sensor", "switch"] diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index 9860334afbf..d7ee57b4f8e 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -28,6 +28,9 @@ from homeassistant.const import ( HTTP_OK, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service + +from . import DOMAIN, PLATFORMS CONF_DATA = "data" CONF_DATA_TEMPLATE = "data_template" @@ -67,6 +70,8 @@ _LOGGER = logging.getLogger(__name__) def get_service(hass, config, discovery_info=None): """Get the RESTful notification service.""" + setup_reload_service(hass, DOMAIN, PLATFORMS) + resource = config.get(CONF_RESOURCE) method = config.get(CONF_METHOD) headers = config.get(CONF_HEADERS) diff --git a/homeassistant/components/rest/services.yaml b/homeassistant/components/rest/services.yaml index 717859d0c12..06baa8734f2 100644 --- a/homeassistant/components/rest/services.yaml +++ b/homeassistant/components/rest/services.yaml @@ -1,2 +1,2 @@ reload: - description: Reload all rest entities. + description: Reload all rest entities and notify services. diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 19a1e46a6d4..89fcf45c29a 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -141,6 +141,12 @@ def async_get_platform( hass: HomeAssistantType, integration_name: str, integration_platform_name: str ) -> Optional[EntityPlatform]: """Find an existing platform.""" + if ( + DATA_ENTITY_PLATFORM not in hass.data + or integration_name not in hass.data[DATA_ENTITY_PLATFORM] + ): + return None + for integration_platform in hass.data[DATA_ENTITY_PLATFORM][integration_name]: if integration_platform.domain == integration_platform_name: platform: EntityPlatform = integration_platform diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py new file mode 100644 index 00000000000..49f3876b97c --- /dev/null +++ b/tests/components/rest/test_notify.py @@ -0,0 +1,52 @@ +"""The tests for the rest.notify platform.""" +from os import path + +from homeassistant import config as hass_config +import homeassistant.components.notify as notify +from homeassistant.components.rest import DOMAIN +from homeassistant.const import SERVICE_RELOAD +from homeassistant.setup import async_setup_component + +from tests.async_mock import patch + + +async def test_reload_notify(hass): + """Verify we can reload the notify service.""" + + assert await async_setup_component( + hass, + notify.DOMAIN, + { + notify.DOMAIN: [ + { + "name": DOMAIN, + "platform": DOMAIN, + "resource": "http://127.0.0.1/off", + }, + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(notify.DOMAIN, DOMAIN) + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "rest/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert not hass.services.has_service(notify.DOMAIN, DOMAIN) + assert hass.services.has_service(notify.DOMAIN, "rest_reloaded") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/rest/configuration.yaml b/tests/fixtures/rest/configuration.yaml index 7848a429026..69a4e771ebf 100644 --- a/tests/fixtures/rest/configuration.yaml +++ b/tests/fixtures/rest/configuration.yaml @@ -3,3 +3,8 @@ sensor: resource: "http://localhost" method: GET name: rollout + +notify: + - name: rest_reloaded + platform: rest + resource: http://127.0.0.1/on From 65b227126da0b91220885f4b68e22a2f2d1e75b5 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 3 Sep 2020 00:06:16 +0000 Subject: [PATCH 586/862] [ci skip] Translation update --- .../accuweather/translations/fr.json | 3 ++ .../accuweather/translations/zh-Hant.json | 2 +- .../alarm_control_panel/translations/fr.json | 2 +- .../deconz/translations/zh-Hant.json | 2 +- .../dialogflow/translations/zh-Hant.json | 2 +- .../ecobee/translations/zh-Hant.json | 2 +- .../emulated_roku/translations/fr.json | 2 +- .../geofency/translations/zh-Hant.json | 4 +- .../gpslogger/translations/zh-Hant.json | 4 +- .../homekit/translations/zh-Hant.json | 12 +++--- .../translations/zh-Hant.json | 2 +- .../components/insteon/translations/ca.json | 3 +- .../components/insteon/translations/fr.json | 5 ++- .../insteon/translations/zh-Hant.json | 4 +- .../translations/zh-Hant.json | 2 +- .../components/kodi/translations/fr.json | 2 +- .../components/konnected/translations/ca.json | 4 +- .../locative/translations/zh-Hant.json | 4 +- .../mailgun/translations/zh-Hant.json | 2 +- .../components/nut/translations/fr.json | 3 +- .../components/nzbget/translations/fr.json | 14 +++++++ .../onvif/translations/zh-Hant.json | 4 +- .../components/openuv/translations/fr.json | 2 +- .../plaato/translations/zh-Hant.json | 4 +- .../components/point/translations/fr.json | 6 +-- .../progettihwsw/translations/ca.json | 8 +++- .../progettihwsw/translations/es.json | 3 ++ .../progettihwsw/translations/fr.json | 12 +++++- .../progettihwsw/translations/it.json | 3 ++ .../progettihwsw/translations/no.json | 3 ++ .../progettihwsw/translations/zh-Hant.json | 3 ++ .../components/roon/translations/fr.json | 2 +- .../components/sentry/translations/fr.json | 1 + .../components/sharkiq/translations/ca.json | 11 ++++- .../components/sharkiq/translations/es.json | 2 +- .../components/sharkiq/translations/fr.json | 29 ++++++++++++++ .../components/sharkiq/translations/it.json | 11 ++++- .../components/sharkiq/translations/no.json | 11 ++++- .../sharkiq/translations/zh-Hant.json | 11 ++++- .../components/shelly/translations/ca.json | 7 ++++ .../components/shelly/translations/es.json | 2 +- .../components/shelly/translations/fr.json | 7 ++++ .../components/shelly/translations/it.json | 7 ++++ .../components/shelly/translations/no.json | 7 ++++ .../shelly/translations/zh-Hant.json | 7 ++++ .../smart_meter_texas/translations/fr.json | 2 +- .../smartthings/translations/fr.json | 2 +- .../speedtestdotnet/translations/zh-Hant.json | 2 +- .../components/spider/translations/fr.json | 7 ++++ .../traccar/translations/zh-Hant.json | 4 +- .../components/tuya/translations/fr.json | 4 +- .../twilio/translations/zh-Hant.json | 2 +- .../components/unifi/translations/fr.json | 4 +- .../vesync/translations/zh-Hant.json | 2 +- .../vizio/translations/zh-Hant.json | 2 +- .../components/yeelight/translations/ca.json | 40 +++++++++++++++++++ .../components/yeelight/translations/en.json | 3 +- .../components/yeelight/translations/fr.json | 1 + .../components/yeelight/translations/ru.json | 3 +- .../components/zwave/translations/fr.json | 2 +- 60 files changed, 260 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/sharkiq/translations/fr.json create mode 100644 homeassistant/components/spider/translations/fr.json create mode 100644 homeassistant/components/yeelight/translations/ca.json diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 5b69afc7334..e5ed21357e0 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, "step": { "user": { "description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration." diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index 9c544b033df..0c6f3a7dc57 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -16,7 +16,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u6574\u5408\u540d\u7a31" }, - "description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u67d0\u4e9b\u50b3\u611f\u5668\u9810\u8a2d\u70ba\u672a\u555f\u7528\uff0c\u53ef\u4ee5\u65bc\u6574\u5408\u8a2d\u5b9a\u4e2d\u555f\u7528\u9019\u4e9b\u7269\u4ef6\u3002\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002", + "description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u67d0\u4e9b\u50b3\u611f\u5668\u9810\u8a2d\u70ba\u672a\u555f\u7528\uff0c\u53ef\u4ee5\u65bc\u6574\u5408\u8a2d\u5b9a\u4e2d\u555f\u7528\u9019\u4e9b\u5be6\u9ad4\u3002\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002", "title": "AccuWeather" } } diff --git a/homeassistant/components/alarm_control_panel/translations/fr.json b/homeassistant/components/alarm_control_panel/translations/fr.json index 597b3d0d2f2..c7e010e805e 100644 --- a/homeassistant/components/alarm_control_panel/translations/fr.json +++ b/homeassistant/components/alarm_control_panel/translations/fr.json @@ -26,7 +26,7 @@ "_": { "armed": "Activ\u00e9", "armed_away": "Enclench\u00e9e (absent)", - "armed_custom_bypass": "Activ\u00e9e avec exception", + "armed_custom_bypass": "Arm\u00e9 avec exception personnalis\u00e9e", "armed_home": "Enclench\u00e9e (pr\u00e9sent)", "armed_night": "Enclench\u00e9 (nuit)", "arming": "Activation", diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index 41711eb81aa..2b941f5ae64 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -6,7 +6,7 @@ "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", "not_deconz_bridge": "\u975e deCONZ Bridge \u8a2d\u5099", "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u7269\u4ef6", - "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u7269\u4ef6" + "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u8a2d\u5099" }, "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" diff --git a/homeassistant/components/dialogflow/translations/zh-Hant.json b/homeassistant/components/dialogflow/translations/zh-Hant.json index a9c9316e600..bf850322480 100644 --- a/homeassistant/components/dialogflow/translations/zh-Hant.json +++ b/homeassistant/components/dialogflow/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Dialogflow \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Dialogflow \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/ecobee/translations/zh-Hant.json b/homeassistant/components/ecobee/translations/zh-Hant.json index db1ffa56c81..ad88a20c56b 100644 --- a/homeassistant/components/ecobee/translations/zh-Hant.json +++ b/homeassistant/components/ecobee/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "one_instance_only": "\u6b64\u6574\u5408\u76ee\u524d\u50c5\u652f\u63f4\u4e00\u7d44 ecobee \u7269\u4ef6" + "one_instance_only": "\u6b64\u6574\u5408\u76ee\u524d\u50c5\u652f\u63f4\u4e00\u7d44 ecobee \u8a2d\u5099\u3002" }, "error": { "pin_request_failed": "ecobee \u6240\u9700\u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d\u5bc6\u9470\u6b63\u78ba\u6027\u3002", diff --git a/homeassistant/components/emulated_roku/translations/fr.json b/homeassistant/components/emulated_roku/translations/fr.json index dd115897e4a..56abe3465d9 100644 --- a/homeassistant/components/emulated_roku/translations/fr.json +++ b/homeassistant/components/emulated_roku/translations/fr.json @@ -17,5 +17,5 @@ } } }, - "title": "EmulatedRoku" + "title": "Emulated Roku" } \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/zh-Hant.json b/homeassistant/components/geofency/translations/zh-Hant.json index 4bf9f6b7158..95fec4ae15e 100644 --- a/homeassistant/components/geofency/translations/zh-Hant.json +++ b/homeassistant/components/geofency/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002", - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + "not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002" }, "create_entry": { "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Geofency \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" diff --git a/homeassistant/components/gpslogger/translations/zh-Hant.json b/homeassistant/components/gpslogger/translations/zh-Hant.json index 9e410084f81..f648d20df9f 100644 --- a/homeassistant/components/gpslogger/translations/zh-Hant.json +++ b/homeassistant/components/gpslogger/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002", - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + "not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002" }, "create_entry": { "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc GPSLogger \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 6fbdfc3aad1..a36ecdff1b2 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -13,7 +13,7 @@ "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", "include_domains": "\u5305\u542b Domain" }, - "description": "HomeKit Bridge \u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u7269\u4ef6\u3002HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c Domain \u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u7269\u4ef6\u3002", + "description": "HomeKit Bridge \u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u5be6\u9ad4\u3002HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c Domain \u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u5be6\u9ad4\u3002", "title": "\u555f\u7528 HomeKit Bridge" } } @@ -37,20 +37,20 @@ }, "exclude": { "data": { - "exclude_entities": "\u6392\u9664\u7269\u4ef6" + "exclude_entities": "\u6392\u9664\u5be6\u9ad4" }, - "description": "\u9078\u64c7\u4e0d\u9032\u884c\u6a4b\u63a5\u7684\u7269\u4ef6\u3002", - "title": "\u65bc\u6240\u9078 Domain \u4e2d\u6240\u8981\u6392\u9664\u7684\u7269\u4ef6" + "description": "\u9078\u64c7\u4e0d\u9032\u884c\u6a4b\u63a5\u7684\u5be6\u9ad4\u3002", + "title": "\u65bc\u6240\u9078 Domain \u4e2d\u6240\u8981\u6392\u9664\u7684\u5be6\u9ad4" }, "init": { "data": { "include_domains": "\u5305\u542b Domain" }, - "description": "\u300c\u5305\u542b Domain\u300d\u4e2d\u7684\u7269\u4ef6\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u7269\u4ef6\u5217\u8868\u3002", + "description": "\u300c\u5305\u542b Domain\u300d\u4e2d\u7684\u5be6\u9ad4\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u5be6\u9ad4\u5217\u8868\u3002", "title": "\u9078\u64c7\u6240\u8981\u6a4b\u63a5\u7684 Domain\u3002" }, "yaml": { - "description": "\u6b64\u7269\u4ef6\u70ba\u900f\u904e YAML \u63a7\u5236", + "description": "\u6b64\u5be6\u9ad4\u70ba\u900f\u904e YAML \u63a7\u5236", "title": "\u8abf\u6574 HomeKit Bridge \u9078\u9805" } } diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index da4b908c840..51f521d5c35 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -6,7 +6,7 @@ "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u8a2d\u5099\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", "ignored_model": "\u7531\u65bc\u6b64\u578b\u865f\u53ef\u539f\u751f\u652f\u63f4\u66f4\u5b8c\u6574\u529f\u80fd\uff0c\u56e0\u6b64 Homekit \u652f\u63f4\u5df2\u88ab\u7981\u6b62\u3002", - "invalid_config_entry": "\u6b64\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", + "invalid_config_entry": "\u6b64\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u5be6\u9ad4\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099" }, "error": { diff --git a/homeassistant/components/insteon/translations/ca.json b/homeassistant/components/insteon/translations/ca.json index 8dd76ba29b8..e1d470b784c 100644 --- a/homeassistant/components/insteon/translations/ca.json +++ b/homeassistant/components/insteon/translations/ca.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Ja hi ha una connexi\u00f3 amb un m\u00f2dem Insteon configurada", - "cannot_connect": "No es pot connectar amb el m\u00f2dem Insteon" + "cannot_connect": "No es pot connectar amb el m\u00f2dem Insteon", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { "cannot_connect": "No s'ha pogut connectar al m\u00f2dem Insteon, torna-ho a provar.", diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index 88ddbb064f8..f44be025271 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Une connexion Insteon par modem est d\u00e9j\u00e0 configur\u00e9e", - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -60,6 +61,7 @@ "data": { "modem_type": "Type de modem." }, + "description": "S\u00e9lectionnez le type de modem Insteon.", "title": "Insteon" } } @@ -77,6 +79,7 @@ "data": { "platform": "Plate-forme" }, + "description": "Modifiez le mot de passe Insteon Hub.", "title": "Insteon" }, "change_hub_config": { diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index c9811c47770..d561bac9aea 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Insteon \u6578\u64da\u6a5f\u9023\u7dda\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -78,7 +78,7 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "input_error": "\u7269\u4ef6\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u503c\u3002", + "input_error": "\u5be6\u9ad4\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u503c\u3002", "select_single": "\u9078\u64c7\u9078\u9805\u3002" }, "step": { diff --git a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json index 34773b88ac0..b9dc6928e01 100644 --- a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json +++ b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002" }, "step": { "user": { diff --git a/homeassistant/components/kodi/translations/fr.json b/homeassistant/components/kodi/translations/fr.json index 9c64ce2efdd..a4fde763d65 100644 --- a/homeassistant/components/kodi/translations/fr.json +++ b/homeassistant/components/kodi/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification non valide", + "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, "flow_title": "Kodi: {name}", diff --git a/homeassistant/components/konnected/translations/ca.json b/homeassistant/components/konnected/translations/ca.json index 2e5ce9b3c98..8938db2621b 100644 --- a/homeassistant/components/konnected/translations/ca.json +++ b/homeassistant/components/konnected/translations/ca.json @@ -32,9 +32,7 @@ "not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io" }, "error": { - "bad_host": "L'URL de sustituci\u00f3 de l'amfitri\u00f3 de l'API \u00e9s inv\u00e0lid", - "one": "Un", - "other": "Altres" + "bad_host": "L'URL de sustituci\u00f3 de l'amfitri\u00f3 de l'API \u00e9s inv\u00e0lid" }, "step": { "options_binary": { diff --git a/homeassistant/components/locative/translations/zh-Hant.json b/homeassistant/components/locative/translations/zh-Hant.json index 53df0ba59eb..3b0089ed220 100644 --- a/homeassistant/components/locative/translations/zh-Hant.json +++ b/homeassistant/components/locative/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002", - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + "not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002" }, "create_entry": { "default": "\u6b32\u50b3\u9001\u5ea7\u6a19\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Locative App \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" diff --git a/homeassistant/components/mailgun/translations/zh-Hant.json b/homeassistant/components/mailgun/translations/zh-Hant.json index d0a31f6fed9..cd39d7a4d83 100644 --- a/homeassistant/components/mailgun/translations/zh-Hant.json +++ b/homeassistant/components/mailgun/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/nut/translations/fr.json b/homeassistant/components/nut/translations/fr.json index bcf2a7679c6..d91bd1e4070 100644 --- a/homeassistant/components/nut/translations/fr.json +++ b/homeassistant/components/nut/translations/fr.json @@ -38,7 +38,8 @@ "data": { "resources": "Ressources", "scan_interval": "Intervalle de balayage (secondes)" - } + }, + "description": "Choisissez les ressources des capteurs." } } } diff --git a/homeassistant/components/nzbget/translations/fr.json b/homeassistant/components/nzbget/translations/fr.json index e77d73c8d38..b08d8c2881a 100644 --- a/homeassistant/components/nzbget/translations/fr.json +++ b/homeassistant/components/nzbget/translations/fr.json @@ -1,9 +1,23 @@ { "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide" + }, + "flow_title": "NZBGet: {name}", "step": { "user": { "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", "ssl": "NZBGet utilise un certificat SSL", + "username": "Nom d'utilisateur", "verify_ssl": "NZBGet utilise un certificat appropri\u00e9" }, "title": "Se connecter \u00e0 NZBGet" diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index c96eb2138f1..794cae79683 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -20,9 +20,9 @@ }, "configure_profile": { "data": { - "include": "\u65b0\u589e\u651d\u5f71\u6a5f\u7269\u4ef6" + "include": "\u65b0\u589e\u651d\u5f71\u6a5f\u5be6\u9ad4" }, - "description": "\u4ee5 {profile} \u4f7f\u7528\u89e3\u6790\u5ea6 {resolution} \u65b0\u589e\u651d\u5f71\u6a5f\u7269\u4ef6\uff1f", + "description": "\u4ee5 {profile} \u4f7f\u7528\u89e3\u6790\u5ea6 {resolution} \u65b0\u589e\u651d\u5f71\u6a5f\u5be6\u9ad4\uff1f", "title": "\u8a2d\u5b9a Profiles" }, "device": { diff --git a/homeassistant/components/openuv/translations/fr.json b/homeassistant/components/openuv/translations/fr.json index f376898b9be..c34aebdee57 100644 --- a/homeassistant/components/openuv/translations/fr.json +++ b/homeassistant/components/openuv/translations/fr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Cl\u00e9 d'API OpenUV", + "api_key": "Cl\u00e9 d'API", "elevation": "Altitude", "latitude": "Latitude", "longitude": "Longitude" diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json index 4ad08ae6805..b374a3111e8 100644 --- a/homeassistant/components/plaato/translations/zh-Hant.json +++ b/homeassistant/components/plaato/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Plaato Airlock \u8a0a\u606f\u3002", - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + "not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Plaato Airlock \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002" }, "create_entry": { "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Plaato Airlock \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index 9fe22410188..141af3545ba 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Vous ne pouvez configurer qu'un compte Point.", + "already_setup": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "external_setup": "Point correctement configur\u00e9 \u00e0 partir d\u2019un autre flux.", @@ -16,14 +16,14 @@ }, "step": { "auth": { - "description": "Suivez le lien ci-dessous et acceptez l'acc\u00e8s \u00e0 votre compte Minut, puis revenez et appuyez sur Envoyer ci-dessous. \n\n [Lien] ( {authorization_url} )", + "description": "Suivez le lien ci-dessous et **acceptez** l'acc\u00e8s \u00e0 votre compte Minut, puis revenez et appuyez sur **Envoyer** ci-dessous. \n\n [Lien] ( {authorization_url} )", "title": "Point d'authentification" }, "user": { "data": { "flow_impl": "Fournisseur" }, - "description": "Voulez-vous commencer la configuration?", + "description": "Voulez-vous commencer la configuration ?", "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } } diff --git a/homeassistant/components/progettihwsw/translations/ca.json b/homeassistant/components/progettihwsw/translations/ca.json index 010de0dc876..00c0545e76a 100644 --- a/homeassistant/components/progettihwsw/translations/ca.json +++ b/homeassistant/components/progettihwsw/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat", @@ -25,7 +28,7 @@ "relay_8": "Rel\u00e9 8", "relay_9": "Rel\u00e9 9" }, - "title": "Configurar rel\u00e9s" + "title": "Configuraci\u00f3 de rel\u00e9s" }, "user": { "data": { @@ -35,5 +38,6 @@ "title": "Configuraci\u00f3 del tauler" } } - } + }, + "title": "Automatitzaci\u00f3 ProgettiHWSW" } \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/es.json b/homeassistant/components/progettihwsw/translations/es.json index a4fa294761d..7e5b6797210 100644 --- a/homeassistant/components/progettihwsw/translations/es.json +++ b/homeassistant/components/progettihwsw/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, "error": { "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado", diff --git a/homeassistant/components/progettihwsw/translations/fr.json b/homeassistant/components/progettihwsw/translations/fr.json index 5a8cc6c8bf6..7a833b27ef2 100644 --- a/homeassistant/components/progettihwsw/translations/fr.json +++ b/homeassistant/components/progettihwsw/translations/fr.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue", + "wrong_info_relay_modes": "La s\u00e9lection du mode de relais doit \u00eatre monostable ou bistable." + }, "step": { "relay_modes": { "data": { @@ -17,12 +25,14 @@ "relay_5": "Relais 5", "relay_6": "Relais 6", "relay_7": "Relais 7", - "relay_8": "Relais 8" + "relay_8": "Relais 8", + "relay_9": "Relais 9" }, "title": "Configurer les relais" }, "user": { "data": { + "host": "H\u00f4te", "port": "Port" }, "title": "Configurer le tableau" diff --git a/homeassistant/components/progettihwsw/translations/it.json b/homeassistant/components/progettihwsw/translations/it.json index 66c40a5a5a7..bad123861f7 100644 --- a/homeassistant/components/progettihwsw/translations/it.json +++ b/homeassistant/components/progettihwsw/translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, "error": { "cannot_connect": "Impossibile connettersi", "unknown": "Errore imprevisto", diff --git a/homeassistant/components/progettihwsw/translations/no.json b/homeassistant/components/progettihwsw/translations/no.json index 66b8348f759..397807ef2fe 100644 --- a/homeassistant/components/progettihwsw/translations/no.json +++ b/homeassistant/components/progettihwsw/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes.", "unknown": "Uventet feil", diff --git a/homeassistant/components/progettihwsw/translations/zh-Hant.json b/homeassistant/components/progettihwsw/translations/zh-Hant.json index 81185a15e2a..c54ff924902 100644 --- a/homeassistant/components/progettihwsw/translations/zh-Hant.json +++ b/homeassistant/components/progettihwsw/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", diff --git a/homeassistant/components/roon/translations/fr.json b/homeassistant/components/roon/translations/fr.json index c282a4cfdbf..f41c7728954 100644 --- a/homeassistant/components/roon/translations/fr.json +++ b/homeassistant/components/roon/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "duplicate_entry": "Cet h\u00f4te a d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9.", - "invalid_auth": "Authentification non valide", + "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/sentry/translations/fr.json b/homeassistant/components/sentry/translations/fr.json index f21e0a645ea..073a3cd0963 100644 --- a/homeassistant/components/sentry/translations/fr.json +++ b/homeassistant/components/sentry/translations/fr.json @@ -27,6 +27,7 @@ "event_handled": "Envoyer des \u00e9v\u00e9nements g\u00e9r\u00e9s", "event_third_party_packages": "Envoyer des \u00e9v\u00e9nements \u00e0 partir de paquets de tiers", "logging_event_level": "Le niveau de journal de Sentry enregistrera un \u00e9v\u00e9nement pour", + "logging_level": "Le niveau de journal de Sentry enregistrera un \u00e9v\u00e9nement pour", "tracing": "Activer le suivi des performances", "tracing_sample_rate": "Taux d'\u00e9chantillonnage de suivi ; entre 0,0 et 1,0 (1,0 = 100%)" } diff --git a/homeassistant/components/sharkiq/translations/ca.json b/homeassistant/components/sharkiq/translations/ca.json index c7a7c13e047..b6e98056863 100644 --- a/homeassistant/components/sharkiq/translations/ca.json +++ b/homeassistant/components/sharkiq/translations/ca.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured_account": "El compte ja ha estat configurat" + "already_configured_account": "El compte ja ha estat configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "reauth_successful": "Token d'acc\u00e9s actualitzat correctament", + "unknown": "Error inesperat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +12,12 @@ "unknown": "Error inesperat" }, "step": { + "reauth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/sharkiq/translations/es.json b/homeassistant/components/sharkiq/translations/es.json index 2d5ff46d50c..a6f70700806 100644 --- a/homeassistant/components/sharkiq/translations/es.json +++ b/homeassistant/components/sharkiq/translations/es.json @@ -15,7 +15,7 @@ "reauth": { "data": { "password": "Contrase\u00f1a", - "username": "Nombre de usuario" + "username": "Usuario" } }, "user": { diff --git a/homeassistant/components/sharkiq/translations/fr.json b/homeassistant/components/sharkiq/translations/fr.json new file mode 100644 index 00000000000..b867170a4d2 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured_account": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/it.json b/homeassistant/components/sharkiq/translations/it.json index a79031ecbc0..8cd3a16bb1c 100644 --- a/homeassistant/components/sharkiq/translations/it.json +++ b/homeassistant/components/sharkiq/translations/it.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured_account": "L'account \u00e8 gi\u00e0 configurato" + "already_configured_account": "L'account \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "reauth_successful": "Token di accesso aggiornato correttamente", + "unknown": "Errore imprevisto" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +12,12 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth": { + "data": { + "password": "Password", + "username": "Nome utente" + } + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/sharkiq/translations/no.json b/homeassistant/components/sharkiq/translations/no.json index 0486219df39..80673ea381b 100644 --- a/homeassistant/components/sharkiq/translations/no.json +++ b/homeassistant/components/sharkiq/translations/no.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured_account": "Kontoen er allerede konfigurert" + "already_configured_account": "Kontoen er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes.", + "reauth_successful": "Tilgangstoken oppdatert vellykket", + "unknown": "Uventet feil" }, "error": { "cannot_connect": "Tilkobling mislyktes.", @@ -9,6 +12,12 @@ "unknown": "Uventet feil" }, "step": { + "reauth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/sharkiq/translations/zh-Hant.json b/homeassistant/components/sharkiq/translations/zh-Hant.json index 42d245709c2..1cb63fe1313 100644 --- a/homeassistant/components/sharkiq/translations/zh-Hant.json +++ b/homeassistant/components/sharkiq/translations/zh-Hant.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "reauth_successful": "\u5b58\u53d6\u5bc6\u9470\u5df2\u6210\u529f\u66f4\u65b0", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +12,12 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index 56e2e816fa3..7e8c3873c11 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -6,6 +6,7 @@ "error": { "auth_not_supported": "Actualment els dispositius Shelly amb autenticaci\u00f3 no son compatibles.", "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "flow_title": "Shelly: {name}", @@ -13,6 +14,12 @@ "confirm_discovery": { "description": "Voleu configurar {model} a {host}?" }, + "credentials": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, "user": { "data": { "host": "Amfitri\u00f3" diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 153c9de33da..bdc05b734ba 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -17,7 +17,7 @@ "credentials": { "data": { "password": "Contrase\u00f1a", - "username": "Nombre de usuario" + "username": "Usuario" } }, "user": { diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index d68b9d86e75..0f62629d21a 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -6,6 +6,7 @@ "error": { "auth_not_supported": "Les appareils Shelly n\u00e9cessitant une authentification ne sont actuellement pas pris en charge.", "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, "flow_title": "Shelly: {name}", @@ -13,6 +14,12 @@ "confirm_discovery": { "description": "Voulez-vous configurer le {model} \u00e0 {host}?" }, + "credentials": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index b74d00a2cb2..595a57b0a00 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -6,6 +6,7 @@ "error": { "auth_not_supported": "I dispositivi Shelly che richiedono l'autenticazione non sono attualmente supportati.", "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "flow_title": "Shelly: {name}", @@ -13,6 +14,12 @@ "confirm_discovery": { "description": "Vuoi impostare {model} su {host}?" }, + "credentials": { + "data": { + "password": "Password", + "username": "Nome utente" + } + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index 8f3b684abc1..898c05e89aa 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -6,6 +6,7 @@ "error": { "auth_not_supported": "Shelly-enheter som krever godkjenning st\u00f8ttes for \u00f8yeblikket ikke.", "cannot_connect": "Tilkobling mislyktes.", + "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "flow_title": "Shelly: {name}", @@ -13,6 +14,12 @@ "confirm_discovery": { "description": "Vil du konfigurere {model} p\u00e5 {host}?" }, + "credentials": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, "user": { "data": { "host": "Vert" diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index ab6dec3cd2d..e8fe857c476 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -6,6 +6,7 @@ "error": { "auth_not_supported": "\u76ee\u524d\u4e0d\u652f\u63f4 Shelly \u8a2d\u5099\u6240\u9700\u8a8d\u8b49\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "Shelly\uff1a{name}", @@ -13,6 +14,12 @@ "confirm_discovery": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model}\uff1f" }, + "credentials": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" diff --git a/homeassistant/components/smart_meter_texas/translations/fr.json b/homeassistant/components/smart_meter_texas/translations/fr.json index 0f11528a087..08e5bb657ee 100644 --- a/homeassistant/components/smart_meter_texas/translations/fr.json +++ b/homeassistant/components/smart_meter_texas/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification non valide", + "invalid_auth": "Authentification invalide", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/smartthings/translations/fr.json b/homeassistant/components/smartthings/translations/fr.json index 9902495e6a1..c355c437689 100644 --- a/homeassistant/components/smartthings/translations/fr.json +++ b/homeassistant/components/smartthings/translations/fr.json @@ -24,7 +24,7 @@ "title": "S\u00e9lectionnez l'emplacement" }, "user": { - "description": "Veuillez entrer un [jeton d'acc\u00e8s personnel SmartThings] ( {token_url} ) cr\u00e9\u00e9 selon les [instructions] ( {component_url} ).", + "description": "SmartThings sera configur\u00e9 pour envoyer des mises \u00e0 jour push \u00e0 Home Assistant \u00e0 l'adresse: \n > {webhook_url} \n\n Si ce n'est pas le cas, mettez \u00e0 jour votre configuration, red\u00e9marrez Home Assistant et r\u00e9essayez.", "title": "Entrer un jeton d'acc\u00e8s personnel" } } diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json index 3f7df7d8ca1..1661030b2f9 100644 --- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json +++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002", "wrong_server_id": "\u4f3a\u670d\u5668 ID \u7121\u6548" }, "step": { diff --git a/homeassistant/components/spider/translations/fr.json b/homeassistant/components/spider/translations/fr.json new file mode 100644 index 00000000000..807ba246694 --- /dev/null +++ b/homeassistant/components/spider/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/zh-Hant.json b/homeassistant/components/traccar/translations/zh-Hant.json index c469020aef6..5135320f631 100644 --- a/homeassistant/components/traccar/translations/zh-Hant.json +++ b/homeassistant/components/traccar/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + "not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u5373\u53ef\u3002" }, "create_entry": { "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Traccar \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u4f7f\u7528 url: `{webhook_url}`\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index fdddb16d9bf..cbd71b0cbd1 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "auth_failed": "Authentification non valide", + "auth_failed": "Authentification invalide", "conn_error": "\u00c9chec de connexion", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "auth_failed": "Authentification non valide" + "auth_failed": "Authentification invalide" }, "flow_title": "Configuration Tuya", "step": { diff --git a/homeassistant/components/twilio/translations/zh-Hant.json b/homeassistant/components/twilio/translations/zh-Hant.json index 8cb926fe259..cc198a4f43d 100644 --- a/homeassistant/components/twilio/translations/zh-Hant.json +++ b/homeassistant/components/twilio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u8a2d\u5099\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 31749e0a78f..e92c33c19de 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -4,8 +4,8 @@ "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur", - "service_unavailable": "Aucun service disponible", + "faulty_credentials": "Authentification invalide", + "service_unavailable": "\u00c9chec de connexion", "unknown_client_mac": "Aucun client disponible sur cette adresse MAC" }, "step": { diff --git a/homeassistant/components/vesync/translations/zh-Hant.json b/homeassistant/components/vesync/translations/zh-Hant.json index efdbee6873c..6d099a9d125 100644 --- a/homeassistant/components/vesync/translations/zh-Hant.json +++ b/homeassistant/components/vesync/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Vesync \u7269\u4ef6" + "already_setup": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Vesync \u8a2d\u5099" }, "error": { "invalid_login": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u7121\u6548" diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index add487060ad..133c2d24381 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "updated_entry": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" + "updated_entry": "\u6b64\u5be6\u9ad4\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u5be6\u9ad4\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/yeelight/translations/ca.json b/homeassistant/components/yeelight/translations/ca.json new file mode 100644 index 00000000000..28c2e796d3c --- /dev/null +++ b/homeassistant/components/yeelight/translations/ca.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "pick_device": { + "data": { + "device": "Dispositiu" + } + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "ip_address": "Adre\u00e7a IP" + }, + "description": "Si deixes l'amfitri\u00f3 buit, s'utilitzar\u00e0 el descobriment per cercar dispositius." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Model (opcional)", + "nightlight_switch": "Utilitza l'interruptor NightLight", + "save_on_change": "Desa l'estat en canviar", + "transition": "Temps de transici\u00f3 (ms)", + "use_music_mode": "Activa el mode M\u00fasica" + }, + "description": "Si deixes el model buit, es detectar\u00e0 autom\u00e0ticament." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/en.json b/homeassistant/components/yeelight/translations/en.json index 602ca9f1399..abb51f13667 100644 --- a/homeassistant/components/yeelight/translations/en.json +++ b/homeassistant/components/yeelight/translations/en.json @@ -15,9 +15,10 @@ }, "user": { "data": { + "host": "Host", "ip_address": "IP Address" }, - "description": "If you leave IP address empty, discovery will be used to find devices." + "description": "If you leave the host empty, discovery will be used to find devices." } } }, diff --git a/homeassistant/components/yeelight/translations/fr.json b/homeassistant/components/yeelight/translations/fr.json index cd7a76ca4df..37770c423ca 100644 --- a/homeassistant/components/yeelight/translations/fr.json +++ b/homeassistant/components/yeelight/translations/fr.json @@ -15,6 +15,7 @@ }, "user": { "data": { + "host": "H\u00f4te", "ip_address": "Adresse IP" }, "description": "Si vous laissez l'adresse IP vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." diff --git a/homeassistant/components/yeelight/translations/ru.json b/homeassistant/components/yeelight/translations/ru.json index 550d8a18472..c078eae7d93 100644 --- a/homeassistant/components/yeelight/translations/ru.json +++ b/homeassistant/components/yeelight/translations/ru.json @@ -15,9 +15,10 @@ }, "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u0415\u0441\u043b\u0438 \u043d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." + "description": "\u0415\u0441\u043b\u0438 \u043d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." } } }, diff --git a/homeassistant/components/zwave/translations/fr.json b/homeassistant/components/zwave/translations/fr.json index 394670cac46..d60a1c9f11e 100644 --- a/homeassistant/components/zwave/translations/fr.json +++ b/homeassistant/components/zwave/translations/fr.json @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "Cl\u00e9 r\u00e9seau (laisser vide pour g\u00e9n\u00e9rer automatiquement)", - "usb_path": "Chemin USB" + "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" }, "description": "Voir https://www.home-assistant.io/docs/z-wave/installation/ pour plus d'informations sur les variables de configuration.", "title": "Configurer Z-Wave" From 93555fed75d921b93ba8299861e31a56fa4bd2a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2020 19:08:47 -0500 Subject: [PATCH 587/862] Support reloading the telegram notify platform (#39529) * Support reloading the telegram notify platform * services.yaml --- homeassistant/components/telegram/__init__.py | 3 ++ homeassistant/components/telegram/notify.py | 5 ++ .../components/telegram/services.yaml | 2 + tests/components/telegram/__init__.py | 1 + tests/components/telegram/test_notify.py | 53 +++++++++++++++++++ tests/fixtures/telegram/configuration.yaml | 4 ++ 6 files changed, 68 insertions(+) create mode 100644 tests/components/telegram/__init__.py create mode 100644 tests/components/telegram/test_notify.py create mode 100644 tests/fixtures/telegram/configuration.yaml diff --git a/homeassistant/components/telegram/__init__.py b/homeassistant/components/telegram/__init__.py index 1aca4e510c6..43a639cb2c3 100644 --- a/homeassistant/components/telegram/__init__.py +++ b/homeassistant/components/telegram/__init__.py @@ -1 +1,4 @@ """The telegram component.""" + +DOMAIN = "telegram" +PLATFORMS = ["notify"] diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index 673935d8283..c0f3f624af9 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -12,6 +12,9 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.const import ATTR_LOCATION +from homeassistant.helpers.reload import setup_reload_service + +from . import DOMAIN as TELEGRAM_DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) @@ -29,6 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_CHAT_ID): vol.Coerce def get_service(hass, config, discovery_info=None): """Get the Telegram notification service.""" + + setup_reload_service(hass, TELEGRAM_DOMAIN, PLATFORMS) chat_id = config.get(CONF_CHAT_ID) return TelegramNotificationService(hass, chat_id) diff --git a/homeassistant/components/telegram/services.yaml b/homeassistant/components/telegram/services.yaml index e69de29bb2d..c467de06fe5 100644 --- a/homeassistant/components/telegram/services.yaml +++ b/homeassistant/components/telegram/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload telegram notify services. diff --git a/tests/components/telegram/__init__.py b/tests/components/telegram/__init__.py new file mode 100644 index 00000000000..feff68fd53a --- /dev/null +++ b/tests/components/telegram/__init__.py @@ -0,0 +1 @@ +"""Tests for telegram component.""" diff --git a/tests/components/telegram/test_notify.py b/tests/components/telegram/test_notify.py new file mode 100644 index 00000000000..7488db49d9e --- /dev/null +++ b/tests/components/telegram/test_notify.py @@ -0,0 +1,53 @@ +"""The tests for the telegram.notify platform.""" +from os import path + +from homeassistant import config as hass_config +import homeassistant.components.notify as notify +from homeassistant.components.telegram import DOMAIN +from homeassistant.const import SERVICE_RELOAD +from homeassistant.setup import async_setup_component + +from tests.async_mock import patch + + +async def test_reload_notify(hass): + """Verify we can reload the notify service.""" + + with patch("homeassistant.components.telegram_bot.async_setup", return_value=True): + assert await async_setup_component( + hass, + notify.DOMAIN, + { + notify.DOMAIN: [ + { + "name": DOMAIN, + "platform": DOMAIN, + "chat_id": 1, + }, + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(notify.DOMAIN, DOMAIN) + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "telegram/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert not hass.services.has_service(notify.DOMAIN, DOMAIN) + assert hass.services.has_service(notify.DOMAIN, "telegram_reloaded") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/telegram/configuration.yaml b/tests/fixtures/telegram/configuration.yaml new file mode 100644 index 00000000000..ab50b4df5ee --- /dev/null +++ b/tests/fixtures/telegram/configuration.yaml @@ -0,0 +1,4 @@ +notify: + - name: telegram_reloaded + platform: telegram + chat_id: 2 From 56e76a326565a34153c3c13f8f793dfa64d67872 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2020 19:43:07 -0500 Subject: [PATCH 588/862] Support reloading the smtp notify platform (#39530) * Support reloading the smtp notify platform * patch test --- homeassistant/components/smtp/__init__.py | 3 ++ homeassistant/components/smtp/notify.py | 4 ++ homeassistant/components/smtp/services.yaml | 2 + tests/components/smtp/test_notify.py | 54 +++++++++++++++++++++ tests/fixtures/smtp/configuration.yaml | 5 ++ 5 files changed, 68 insertions(+) create mode 100644 homeassistant/components/smtp/services.yaml create mode 100644 tests/fixtures/smtp/configuration.yaml diff --git a/homeassistant/components/smtp/__init__.py b/homeassistant/components/smtp/__init__.py index 5e7fb41c212..7461dd2b6c3 100644 --- a/homeassistant/components/smtp/__init__.py +++ b/homeassistant/components/smtp/__init__.py @@ -1 +1,4 @@ """The smtp component.""" + +DOMAIN = "smtp" +PLATFORMS = ["notify"] diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 296a419c5d0..f7d3415525d 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -26,8 +26,11 @@ from homeassistant.const import ( CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.reload import setup_reload_service import homeassistant.util.dt as dt_util +from . import DOMAIN, PLATFORMS + _LOGGER = logging.getLogger(__name__) ATTR_IMAGES = "images" # optional embedded image file attachments @@ -67,6 +70,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the mail notification service.""" + setup_reload_service(hass, DOMAIN, PLATFORMS) mail_service = MailNotificationService( config.get(CONF_SERVER), config.get(CONF_PORT), diff --git a/homeassistant/components/smtp/services.yaml b/homeassistant/components/smtp/services.yaml new file mode 100644 index 00000000000..77ff7d22adf --- /dev/null +++ b/homeassistant/components/smtp/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload smtp notify services. diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index d99ee82d4ef..6320614c059 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -1,8 +1,14 @@ """The tests for the notify smtp platform.""" +from os import path import re import unittest +from homeassistant import config as hass_config +import homeassistant.components.notify as notify +from homeassistant.components.smtp import DOMAIN from homeassistant.components.smtp.notify import MailNotificationService +from homeassistant.const import SERVICE_RELOAD +from homeassistant.setup import async_setup_component from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -85,3 +91,51 @@ class TestNotifySmtp(unittest.TestCase): "Test msg", data={"html": html, "images": ["test.jpg"]} ) assert "Content-Type: multipart/related" in msg + + +async def test_reload_notify(hass): + """Verify we can reload the notify service.""" + + with patch( + "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid" + ): + assert await async_setup_component( + hass, + notify.DOMAIN, + { + notify.DOMAIN: [ + { + "name": DOMAIN, + "platform": DOMAIN, + "recipient": "test@example.com", + "sender": "test@example.com", + }, + ] + }, + ) + await hass.async_block_till_done() + + assert hass.services.has_service(notify.DOMAIN, DOMAIN) + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "smtp/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( + "homeassistant.components.smtp.notify.MailNotificationService.connection_is_valid" + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert not hass.services.has_service(notify.DOMAIN, DOMAIN) + assert hass.services.has_service(notify.DOMAIN, "smtp_reloaded") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/smtp/configuration.yaml b/tests/fixtures/smtp/configuration.yaml new file mode 100644 index 00000000000..03bed686909 --- /dev/null +++ b/tests/fixtures/smtp/configuration.yaml @@ -0,0 +1,5 @@ +notify: + - name: smtp_reloaded + platform: smtp + sender: test@example.com + recipient: test@example.com From 65e53b825111531e675b0b615cec4dc2e89b11d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2020 20:16:21 -0500 Subject: [PATCH 589/862] Support reloading mqtt yaml configuration (#39531) --- homeassistant/components/mqtt/__init__.py | 14 ++++++ .../components/mqtt/alarm_control_panel.py | 4 ++ .../components/mqtt/binary_sensor.py | 4 ++ homeassistant/components/mqtt/camera.py | 4 ++ homeassistant/components/mqtt/climate.py | 4 ++ homeassistant/components/mqtt/cover.py | 4 ++ homeassistant/components/mqtt/fan.py | 4 ++ .../components/mqtt/light/__init__.py | 3 ++ homeassistant/components/mqtt/lock.py | 4 ++ homeassistant/components/mqtt/sensor.py | 4 ++ homeassistant/components/mqtt/services.yaml | 3 ++ homeassistant/components/mqtt/switch.py | 4 ++ .../components/mqtt/vacuum/__init__.py | 3 ++ tests/components/mqtt/test_light.py | 45 ++++++++++++++++++- tests/fixtures/mqtt/configuration.yaml | 4 ++ 15 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/mqtt/configuration.yaml diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 869dc27cfd6..d4263ee6ba3 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -136,6 +136,20 @@ CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" DISCOVERY_COOLDOWN = 2 TIMEOUT_ACK = 1 +PLATFORMS = [ + "alarm_control_panel", + "binary_sensor", + "camera", + "climate", + "cover", + "fan", + "light", + "lock", + "sensor", + "switch", + "vacuum", +] + def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType: """Validate that a device info entry has at least one identifying value.""" diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 35c9d028c52..505c7616a0a 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -31,6 +31,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( @@ -39,6 +40,8 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -100,6 +103,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT alarm control panel through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(config, async_add_entities) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 9afb9401bfe..c9fd2bba2b1 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -24,6 +24,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import dt as dt_util @@ -31,6 +32,8 @@ from . import ( ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -72,6 +75,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT binary sensor through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(config, async_add_entities) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index d0311d5c694..82e5cb8b272 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -9,11 +9,14 @@ from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( ATTR_DISCOVERY_HASH, CONF_QOS, + DOMAIN, + PLATFORMS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -46,6 +49,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT camera through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(config, async_add_entities) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 20ee183d693..68579559e35 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -48,13 +48,16 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, + DOMAIN, MQTT_BASE_PLATFORM_SCHEMA, + PLATFORMS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -239,6 +242,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT climate device through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(hass, config, async_add_entities) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 772c2900eed..b8a5b778a98 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -34,6 +34,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( @@ -42,6 +43,8 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -170,6 +173,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT cover through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(config, async_add_entities) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index e70ff62b0fb..b1ec7aabeef 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -26,6 +26,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( @@ -34,6 +35,8 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -111,6 +114,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT fan through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(config, async_add_entities) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index fe33756e51e..2375fb86e5a 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -10,8 +10,10 @@ from homeassistant.components.mqtt.discovery import ( clear_discovery_hash, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from .. import DOMAIN, PLATFORMS from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json @@ -39,6 +41,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT light through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(hass, config, async_add_entities) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 744eeb17e6f..f6d56a30431 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -15,6 +15,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( @@ -23,6 +24,8 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -73,6 +76,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT lock panel through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(config, async_add_entities) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index f2cb8f22e84..eb241bba7a0 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -22,6 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import dt as dt_util @@ -29,6 +30,8 @@ from . import ( ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -66,6 +69,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT sensors through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(config, async_add_entities) diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index 2af3c22fe50..04dce23f5de 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -35,3 +35,6 @@ dump: description: how long we should listen for messages in seconds example: 5 default: 5 + +reload: + description: Reload all mqtt entities from yaml. diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 8a164019f10..364f060ac14 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -19,6 +19,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -28,6 +29,8 @@ from . import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, @@ -69,6 +72,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): """Set up MQTT switch through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) await _async_setup_entity(config, async_add_entities, discovery_info) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index b16ec7aaf74..b954a97e8f9 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -10,7 +10,9 @@ from homeassistant.components.mqtt.discovery import ( ) from homeassistant.components.vacuum import DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.reload import async_setup_reload_service +from .. import DOMAIN as MQTT_DOMAIN, PLATFORMS from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state @@ -31,6 +33,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up MQTT vacuum through configuration.yaml.""" + await async_setup_reload_service(hass, MQTT_DOMAIN, PLATFORMS) await _async_setup_entity(config, async_add_entities, discovery_info) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index d83cd7fda7f..56a5b4012c8 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -153,11 +153,14 @@ light: payload_off: "off" """ +from os import path + import pytest +from homeassistant import config as hass_config from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ASSUMED_STATE, SERVICE_RELOAD, STATE_OFF, STATE_ON import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -1558,3 +1561,43 @@ async def test_max_mireds(hass, mqtt_mock): state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 assert state.attributes.get("max_mireds") == 370 + + +async def test_reloadable(hass, mqtt_mock): + """Test reloading an mqtt light.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test/set", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + assert hass.states.get("light.test") + assert len(hass.states.async_all()) == 1 + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "mqtt/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "mqtt", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("light.test") is None + assert hass.states.get("light.reload") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/mqtt/configuration.yaml b/tests/fixtures/mqtt/configuration.yaml new file mode 100644 index 00000000000..96c7e57f72b --- /dev/null +++ b/tests/fixtures/mqtt/configuration.yaml @@ -0,0 +1,4 @@ +light: + - platform: mqtt + name: reload + command_topic: "test/set" From 49daf57c54d5ced03af7009098ef5d75046ab528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hendrik=20Schr=C3=B6ter?= Date: Thu, 3 Sep 2020 06:16:38 +0000 Subject: [PATCH 590/862] Fix rain retrival for OWM (#39566) As documented in the OWM API (https://openweathermap.org/api/one-call-api), rain and snow are reported on a 1h basis: current.rain current.rain.1h (where available) Rain volume for last hour, mm current.snow current.snow.1h (where available) Snow volume for last hour, mm --- homeassistant/components/openweathermap/sensor.py | 9 +++++---- homeassistant/components/openweathermap/weather.py | 10 ++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index b730923f0bd..938fb609158 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -165,15 +165,16 @@ class OpenWeatherMapSensor(Entity): self._state = data.get_clouds() elif self.type == "rain": rain = data.get_rain() - if "3h" in rain: - self._state = round(rain["3h"], 0) + if "1h" in rain: + self._state = round(rain["1h"], 0) self._unit_of_measurement = "mm" else: self._state = "not raining" self._unit_of_measurement = "" elif self.type == "snow": - if data.get_snow(): - self._state = round(data.get_snow(), 0) + snow = data.get_snow() + if "1h" in snow: + self._state = round(snow["1h"], 0) self._unit_of_measurement = "mm" else: self._state = "not snowing" diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index ce8676ad440..7200e759d3b 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -203,18 +203,16 @@ class OpenWeatherMapWeather(WeatherEntity): } ) else: + rain = entry.get_rain().get("1h") + if rain is not None: + rain = round(rain, 1) data.append( { ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000, ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get( "temp" ), - ATTR_FORECAST_PRECIPITATION: ( - round(entry.get_rain().get("3h"), 1) - if entry.get_rain().get("3h") is not None - and (round(entry.get_rain().get("3h"), 1) > 0) - else None - ), + ATTR_FORECAST_PRECIPITATION: rain, ATTR_FORECAST_CONDITION: [ k for k, v in CONDITION_CLASSES.items() From d66bc6a873d1010833999bdae5dfe06732846bef Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Sep 2020 02:26:30 -0500 Subject: [PATCH 591/862] Add Recently Added and On Deck to Plex media browser (#39232) --- .../components/plex/media_browser.py | 55 ++++++++++++++++- tests/components/plex/mock_classes.py | 16 +++++ tests/components/plex/test_browse_media.py | 60 +++++++++++++++++-- 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index ac316edb938..e8d5d32c66e 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -13,6 +13,10 @@ PLAYLISTS_BROWSE_PAYLOAD = { "can_play": False, "can_expand": True, } +SPECIAL_METHODS = { + "On Deck": "onDeck", + "Recently Added": "recentlyAdded", +} _LOGGER = logging.getLogger(__name__) @@ -36,14 +40,43 @@ def browse_media( media_info["children"].append(item_payload(item)) return media_info + if media_content_id and ":" in media_content_id: + media_content_id, special_folder = media_content_id.split(":") + else: + special_folder = None + if ( - media_content_type == "server" + media_content_type + and media_content_type == "server" and media_content_id != plex_server.machine_identifier ): raise BrowseError( f"Plex server with ID '{media_content_id}' is not associated with {entity_id}" ) + if special_folder: + if media_content_type == "server": + library_or_section = plex_server.library + title = plex_server.friendly_name + elif media_content_type == "library": + library_or_section = plex_server.library.sectionByID(media_content_id) + title = library_or_section.title + + payload = { + "title": title, + "media_content_id": f"{media_content_id}:{special_folder}", + "media_content_type": media_content_type, + "can_play": False, + "can_expand": True, + "children": [], + } + + method = SPECIAL_METHODS[special_folder] + items = getattr(library_or_section, method)() + for item in items: + payload["children"].append(item_payload(item)) + return payload + if media_content_type in ["server", None]: return server_payload(plex_server) @@ -89,6 +122,18 @@ def library_section_payload(section): } +def special_library_payload(parent_payload, special_type): + """Create response payload for special library folders.""" + title = f"{special_type} ({parent_payload['title']})" + return { + "title": title, + "media_content_id": f"{parent_payload['media_content_id']}:{special_type}", + "media_content_type": parent_payload["media_content_type"], + "can_play": False, + "can_expand": True, + } + + def server_payload(plex_server): """Create response payload to describe libraries of the Plex server.""" server_info = { @@ -99,6 +144,10 @@ def server_payload(plex_server): "can_expand": True, } server_info["children"] = [] + server_info["children"].append(special_library_payload(server_info, "On Deck")) + server_info["children"].append( + special_library_payload(server_info, "Recently Added") + ) for library in plex_server.library.sections(): if library.type == "photo": continue @@ -112,6 +161,10 @@ def library_payload(plex_server, library_id): library = plex_server.library.sectionByID(library_id) library_info = library_section_payload(library) library_info["children"] = [] + library_info["children"].append(special_library_payload(library_info, "On Deck")) + library_info["children"].append( + special_library_payload(library_info, "Recently Added") + ) for item in library.all(): library_info["children"].append(item_payload(item)) return library_info diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 287712cf520..1fc705be1ca 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -346,6 +346,14 @@ class MockPlexLibrary: """Mock the sectionByID lookup.""" return [x for x in self.sections() if x.key == section_id][0] + def onDeck(self): + """Mock an empty On Deck folder.""" + return [] + + def recentlyAdded(self): + """Mock an empty Recently Added folder.""" + return [] + class MockPlexLibrarySection: """Mock a Plex LibrarySection instance.""" @@ -381,6 +389,14 @@ class MockPlexLibrarySection: if child.ratingKey == ratingKey: return child + def onDeck(self): + """Mock an empty On Deck folder.""" + return [] + + def recentlyAdded(self): + """Mock an empty Recently Added folder.""" + return self.all() + @property def type(self): """Mock the library type.""" diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index 005fe4c9e44..9cf6d7a7332 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -4,6 +4,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, ) from homeassistant.components.plex.const import CONF_SERVER_IDENTIFIER, DOMAIN +from homeassistant.components.plex.media_browser import SPECIAL_METHODS from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE_RESULT from .const import DEFAULT_DATA, DEFAULT_OPTIONS @@ -77,10 +78,61 @@ async def test_browse_media(hass, hass_ws_client): result = msg["result"] assert result[ATTR_MEDIA_CONTENT_TYPE] == "server" assert result[ATTR_MEDIA_CONTENT_ID] == DEFAULT_DATA[CONF_SERVER_IDENTIFIER] - assert len(result["children"]) == len(mock_plex_server.library.sections()) + assert len(result["children"]) == len(mock_plex_server.library.sections()) + len( + SPECIAL_METHODS + ) tvshows = next(iter(x for x in result["children"] if x["title"] == "TV Shows")) playlists = next(iter(x for x in result["children"] if x["title"] == "Playlists")) + special_keys = list(SPECIAL_METHODS.keys()) + + # Browse into a special folder (server) + msg_id += 1 + await websocket_client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": media_players[0], + ATTR_MEDIA_CONTENT_TYPE: "server", + ATTR_MEDIA_CONTENT_ID: f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}:{special_keys[0]}", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == msg_id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + result = msg["result"] + assert result[ATTR_MEDIA_CONTENT_TYPE] == "server" + assert ( + result[ATTR_MEDIA_CONTENT_ID] + == f"{DEFAULT_DATA[CONF_SERVER_IDENTIFIER]}:{special_keys[0]}" + ) + assert len(result["children"]) == len(mock_plex_server.library.onDeck()) + + # Browse into a special folder (library) + msg_id += 1 + library_section_id = next(iter(mock_plex_server.library.sections())).key + await websocket_client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": media_players[0], + ATTR_MEDIA_CONTENT_TYPE: "library", + ATTR_MEDIA_CONTENT_ID: f"{library_section_id}:{special_keys[1]}", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == msg_id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + result = msg["result"] + assert result[ATTR_MEDIA_CONTENT_TYPE] == "library" + assert result[ATTR_MEDIA_CONTENT_ID] == f"{library_section_id}:{special_keys[1]}" + assert len(result["children"]) == len( + mock_plex_server.library.sectionByID(library_section_id).recentlyAdded() + ) # Browse into a Plex TV show library msg_id += 1 @@ -103,7 +155,7 @@ async def test_browse_media(hass, hass_ws_client): result_id = result[ATTR_MEDIA_CONTENT_ID] assert len(result["children"]) == len( mock_plex_server.library.sectionByID(result_id).all() - ) + ) + len(SPECIAL_METHODS) # Browse into a Plex TV show msg_id += 1 @@ -112,8 +164,8 @@ async def test_browse_media(hass, hass_ws_client): "id": msg_id, "type": "media_player/browse_media", "entity_id": media_players[0], - ATTR_MEDIA_CONTENT_TYPE: result["children"][0][ATTR_MEDIA_CONTENT_TYPE], - ATTR_MEDIA_CONTENT_ID: str(result["children"][0][ATTR_MEDIA_CONTENT_ID]), + ATTR_MEDIA_CONTENT_TYPE: result["children"][-1][ATTR_MEDIA_CONTENT_TYPE], + ATTR_MEDIA_CONTENT_ID: str(result["children"][-1][ATTR_MEDIA_CONTENT_ID]), } ) From 226406b8537121a250212639f2b2afd59017e18d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 3 Sep 2020 09:27:21 +0200 Subject: [PATCH 592/862] Improve tests for GIOS integration (#39514) --- .coveragerc | 2 - homeassistant/components/gios/air_quality.py | 2 + homeassistant/components/gios/manifest.json | 3 +- tests/components/gios/__init__.py | 46 +++++++ tests/components/gios/test_air_quality.py | 123 +++++++++++++++++++ tests/components/gios/test_config_flow.py | 54 ++++---- tests/components/gios/test_init.py | 54 ++++++++ tests/fixtures/gios/indexes.json | 29 +++++ tests/fixtures/gios/sensors.json | 51 ++++++++ tests/fixtures/gios/station.json | 72 +++++++++++ 10 files changed, 406 insertions(+), 30 deletions(-) create mode 100644 tests/components/gios/test_air_quality.py create mode 100644 tests/components/gios/test_init.py create mode 100644 tests/fixtures/gios/indexes.json create mode 100644 tests/fixtures/gios/sensors.json create mode 100644 tests/fixtures/gios/station.json diff --git a/.coveragerc b/.coveragerc index 2ec685df4ce..fdfb25be56b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -306,8 +306,6 @@ omit = homeassistant/components/gc100/* homeassistant/components/geniushub/* homeassistant/components/geizhals/sensor.py - homeassistant/components/gios/__init__.py - homeassistant/components/gios/air_quality.py homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py index 8fcd84a4622..2853570ce58 100644 --- a/homeassistant/components/gios/air_quality.py +++ b/homeassistant/components/gios/air_quality.py @@ -24,6 +24,8 @@ SENSOR_MAP = { "SO2": ATTR_SO2, } +PARALLEL_UPDATES = 1 + async def async_setup_entry(hass, config_entry, async_add_entities): """Add a GIOS entities from a config_entry.""" diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index da4ceddbeb5..0fc544c9d1e 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], "requirements": ["gios==0.1.3"], - "config_flow": true + "config_flow": true, + "quality_scale": "platinum" } diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 98528fda9f9..920461a6ae1 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -1 +1,47 @@ """Tests for GIOS.""" +import json + +from homeassistant.components.gios.const import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry, load_fixture + +STATIONS = [ + {"id": 123, "stationName": "Test Name 1", "gegrLat": "99.99", "gegrLon": "88.88"}, + {"id": 321, "stationName": "Test Name 2", "gegrLat": "77.77", "gegrLon": "66.66"}, +] + + +async def init_integration(hass, incomplete_data=False) -> MockConfigEntry: + """Set up the GIOS integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id=123, + data={"station_id": 123, "name": "Home"}, + ) + + indexes = json.loads(load_fixture("gios/indexes.json")) + station = json.loads(load_fixture("gios/station.json")) + sensors = json.loads(load_fixture("gios/sensors.json")) + if incomplete_data: + indexes["stIndexLevel"]["indexLevelName"] = "foo" + sensors["PM10"]["values"][0]["value"] = None + sensors["PM10"]["values"][1]["value"] = None + + with patch( + "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS + ), patch( + "homeassistant.components.gios.Gios._get_station", + return_value=station, + ), patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=sensors, + ), patch( + "homeassistant.components.gios.Gios._get_indexes", return_value=indexes + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/gios/test_air_quality.py b/tests/components/gios/test_air_quality.py new file mode 100644 index 00000000000..9a5b1be6a20 --- /dev/null +++ b/tests/components/gios/test_air_quality.py @@ -0,0 +1,123 @@ +"""Test air_quality of GIOS integration.""" +from datetime import timedelta +import json + +from gios import ApiError + +from homeassistant.components.air_quality import ( + ATTR_AQI, + ATTR_CO, + ATTR_NO2, + ATTR_OZONE, + ATTR_PM_2_5, + ATTR_PM_10, + ATTR_SO2, +) +from homeassistant.components.gios.air_quality import ATTRIBUTION +from homeassistant.components.gios.const import AQI_GOOD +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + STATE_UNAVAILABLE, +) +from homeassistant.util.dt import utcnow + +from tests.async_mock import patch +from tests.common import async_fire_time_changed, load_fixture +from tests.components.gios import init_integration + + +async def test_air_quality(hass): + """Test states of the air_quality.""" + await init_integration(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("air_quality.home") + assert state + assert state.state == "4" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_AQI) == AQI_GOOD + assert state.attributes.get(ATTR_PM_10) == 17 + assert state.attributes.get(ATTR_PM_2_5) == 4 + assert state.attributes.get(ATTR_CO) == 252 + assert state.attributes.get(ATTR_SO2) == 4 + assert state.attributes.get(ATTR_NO2) == 7 + assert state.attributes.get(ATTR_OZONE) == 96 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:emoticon-happy" + assert state.attributes.get("station") == "Test Name 1" + + entry = registry.async_get("air_quality.home") + assert entry + assert entry.unique_id == 123 + + +async def test_air_quality_with_incomplete_data(hass): + """Test states of the air_quality with incomplete data from measuring station.""" + await init_integration(hass, incomplete_data=True) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("air_quality.home") + assert state + assert state.state == "4" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_AQI) == "foo" + assert state.attributes.get(ATTR_PM_10) is None + assert state.attributes.get(ATTR_PM_2_5) == 4 + assert state.attributes.get(ATTR_CO) == 252 + assert state.attributes.get(ATTR_SO2) == 4 + assert state.attributes.get(ATTR_NO2) == 7 + assert state.attributes.get(ATTR_OZONE) == 96 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + assert state.attributes.get(ATTR_ICON) == "mdi:blur" + assert state.attributes.get("station") == "Test Name 1" + + entry = registry.async_get("air_quality.home") + assert entry + assert entry.unique_id == 123 + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + await init_integration(hass) + + state = hass.states.get("air_quality.home") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "4" + + future = utcnow() + timedelta(minutes=60) + with patch( + "homeassistant.components.gios.Gios._get_all_sensors", + side_effect=ApiError("Unexpected error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.home") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=120) + with patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=json.loads(load_fixture("gios/sensors.json")), + ), patch( + "homeassistant.components.gios.Gios._get_indexes", + return_value=json.loads(load_fixture("gios/indexes.json")), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.home") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "4" diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index b2f7ceec9e4..24ada20aded 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the GIOS config flow.""" +import json + from gios import ApiError from homeassistant import data_entry_flow @@ -7,28 +9,14 @@ from homeassistant.components.gios.const import CONF_STATION_ID from homeassistant.const import CONF_NAME from tests.async_mock import patch +from tests.common import load_fixture +from tests.components.gios import STATIONS CONFIG = { CONF_NAME: "Foo", CONF_STATION_ID: 123, } -VALID_STATIONS = [ - {"id": 123, "stationName": "Test Name 1", "gegrLat": "99.99", "gegrLon": "88.88"}, - {"id": 321, "stationName": "Test Name 2", "gegrLat": "77.77", "gegrLon": "66.66"}, -] - -VALID_STATION = [ - {"id": 3764, "param": {"paramName": "particulate matter PM10", "paramCode": "PM10"}} -] - -VALID_INDEXES = { - "stIndexLevel": {"id": 1, "indexLevelName": "Good"}, - "pm10IndexLevel": {"id": 0, "indexLevelName": "Very good"}, -} - -VALID_SENSOR = {"key": "PM10", "values": [{"value": 11.11}]} - async def test_show_form(hass): """Test that the form is served with no input.""" @@ -43,7 +31,9 @@ async def test_show_form(hass): async def test_invalid_station_id(hass): """Test that errors are shown when measuring station ID is invalid.""" - with patch("gios.Gios._get_stations", return_value=VALID_STATIONS): + with patch( + "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS + ): flow = config_flow.GiosFlowHandler() flow.hass = hass flow.context = {} @@ -57,10 +47,13 @@ async def test_invalid_station_id(hass): async def test_invalid_sensor_data(hass): """Test that errors are shown when sensor data is invalid.""" - with patch("gios.Gios._get_stations", return_value=VALID_STATIONS), patch( - "gios.Gios._get_station", return_value=VALID_STATION - ), patch("gios.Gios._get_station", return_value=VALID_STATION), patch( - "gios.Gios._get_sensor", return_value={} + with patch( + "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS + ), patch( + "homeassistant.components.gios.Gios._get_station", + return_value=json.loads(load_fixture("gios/station.json")), + ), patch( + "homeassistant.components.gios.Gios._get_sensor", return_value={} ): flow = config_flow.GiosFlowHandler() flow.hass = hass @@ -73,7 +66,9 @@ async def test_invalid_sensor_data(hass): async def test_cannot_connect(hass): """Test that errors are shown when cannot connect to GIOS server.""" - with patch("gios.Gios._async_get", side_effect=ApiError("error")): + with patch( + "homeassistant.components.gios.Gios._async_get", side_effect=ApiError("error") + ): flow = config_flow.GiosFlowHandler() flow.hass = hass flow.context = {} @@ -85,12 +80,17 @@ async def test_cannot_connect(hass): async def test_create_entry(hass): """Test that the user step works.""" - with patch("gios.Gios._get_stations", return_value=VALID_STATIONS), patch( - "gios.Gios._get_station", return_value=VALID_STATION - ), patch("gios.Gios._get_station", return_value=VALID_STATION), patch( - "gios.Gios._get_sensor", return_value=VALID_SENSOR + with patch( + "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS ), patch( - "gios.Gios._get_indexes", return_value=VALID_INDEXES + "homeassistant.components.gios.Gios._get_station", + return_value=json.loads(load_fixture("gios/station.json")), + ), patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=json.loads(load_fixture("gios/sensors.json")), + ), patch( + "homeassistant.components.gios.Gios._get_indexes", + return_value=json.loads(load_fixture("gios/indexes.json")), ): flow = config_flow.GiosFlowHandler() flow.hass = hass diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py new file mode 100644 index 00000000000..0846ddfb4ca --- /dev/null +++ b/tests/components/gios/test_init.py @@ -0,0 +1,54 @@ +"""Test init of GIOS integration.""" +from homeassistant.components.gios.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import STATE_UNAVAILABLE + +from tests.async_mock import patch +from tests.common import MockConfigEntry +from tests.components.gios import init_integration + + +async def test_async_setup_entry(hass): + """Test a successful setup entry.""" + await init_integration(hass) + + state = hass.states.get("air_quality.home") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "4" + + +async def test_config_not_ready(hass): + """Test for setup failure if connection to GIOS is missing.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id=123, + data={"station_id": 123, "name": "Home"}, + ) + + with patch( + "homeassistant.components.gios.Gios._get_stations", + side_effect=ConnectionError(), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/fixtures/gios/indexes.json b/tests/fixtures/gios/indexes.json new file mode 100644 index 00000000000..4fe4293706c --- /dev/null +++ b/tests/fixtures/gios/indexes.json @@ -0,0 +1,29 @@ +{ + "id": 123, + "stCalcDate": "2020-07-31 15:10:17", + "stIndexLevel": { "id": 1, "indexLevelName": "dobry" }, + "stSourceDataDate": "2020-07-31 14:00:00", + "so2CalcDate": "2020-07-31 15:10:17", + "so2IndexLevel": { "id": 0, "indexLevelName": "bardzo dobry" }, + "so2SourceDataDate": "2020-07-31 14:00:00", + "no2CalcDate": 1596201017000, + "no2IndexLevel": { "id": 0, "indexLevelName": "dobry" }, + "no2SourceDataDate": "2020-07-31 14:00:00", + "coCalcDate": "2020-07-31 15:10:17", + "coIndexLevel": { "id": 0, "indexLevelName": "dobry" }, + "coSourceDataDate": "2020-07-31 14:00:00", + "pm10CalcDate": "2020-07-31 15:10:17", + "pm10IndexLevel": { "id": 0, "indexLevelName": "dobry" }, + "pm10SourceDataDate": "2020-07-31 14:00:00", + "pm25CalcDate": "2020-07-31 15:10:17", + "pm25IndexLevel": { "id": 0, "indexLevelName": "dobry" }, + "pm25SourceDataDate": "2020-07-31 14:00:00", + "o3CalcDate": "2020-07-31 15:10:17", + "o3IndexLevel": { "id": 1, "indexLevelName": "dobry" }, + "o3SourceDataDate": "2020-07-31 14:00:00", + "c6h6CalcDate": "2020-07-31 15:10:17", + "c6h6IndexLevel": { "id": 0, "indexLevelName": "bardzo dobry" }, + "c6h6SourceDataDate": "2020-07-31 14:00:00", + "stIndexStatus": true, + "stIndexCrParam": "OZON" + } \ No newline at end of file diff --git a/tests/fixtures/gios/sensors.json b/tests/fixtures/gios/sensors.json new file mode 100644 index 00000000000..3103a2bf16e --- /dev/null +++ b/tests/fixtures/gios/sensors.json @@ -0,0 +1,51 @@ +{ + "SO2": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 4.35478 }, + { "date": "2020-07-31 14:00:00", "value": 4.25478 }, + { "date": "2020-07-31 13:00:00", "value": 4.34309 } + ] + }, + "C6H6": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 0.23789 }, + { "date": "2020-07-31 14:00:00", "value": 0.22789 }, + { "date": "2020-07-31 13:00:00", "value": 0.21315 } + ] + }, + "CO": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 251.874 }, + { "date": "2020-07-31 14:00:00", "value": 250.874 }, + { "date": "2020-07-31 13:00:00", "value": 251.097 } + ] + }, + "NO2": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 7.13411 }, + { "date": "2020-07-31 14:00:00", "value": 7.33411 }, + { "date": "2020-07-31 13:00:00", "value": 9.32578 } + ] + }, + "O3": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 95.7768 }, + { "date": "2020-07-31 14:00:00", "value": 93.7768 }, + { "date": "2020-07-31 13:00:00", "value": 89.4232 } + ] + }, + "PM2.5": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 4 }, + { "date": "2020-07-31 14:00:00", "value": 4 }, + { "date": "2020-07-31 13:00:00", "value": 5 } + ] + }, + "PM10": { + "values": [ + { "date": "2020-07-31 15:00:00", "value": 16.8344 }, + { "date": "2020-07-31 14:00:00", "value": 17.8344 }, + { "date": "2020-07-31 13:00:00", "value": 20.8094 } + ] + } + } \ No newline at end of file diff --git a/tests/fixtures/gios/station.json b/tests/fixtures/gios/station.json new file mode 100644 index 00000000000..0eaa98a1d3c --- /dev/null +++ b/tests/fixtures/gios/station.json @@ -0,0 +1,72 @@ +[ + { + "id": 672, + "stationId": 117, + "param": { + "paramName": "dwutlenek siarki", + "paramFormula": "SO2", + "paramCode": "SO2", + "idParam": 1 + } + }, + { + "id": 658, + "stationId": 117, + "param": { + "paramName": "benzen", + "paramFormula": "C6H6", + "paramCode": "C6H6", + "idParam": 10 + } + }, + { + "id": 660, + "stationId": 117, + "param": { + "paramName": "tlenek węgla", + "paramFormula": "CO", + "paramCode": "CO", + "idParam": 8 + } + }, + { + "id": 665, + "stationId": 117, + "param": { + "paramName": "dwutlenek azotu", + "paramFormula": "NO2", + "paramCode": "NO2", + "idParam": 6 + } + }, + { + "id": 667, + "stationId": 117, + "param": { + "paramName": "ozon", + "paramFormula": "O3", + "paramCode": "O3", + "idParam": 5 + } + }, + { + "id": 670, + "stationId": 117, + "param": { + "paramName": "pył zawieszony PM2.5", + "paramFormula": "PM2.5", + "paramCode": "PM2.5", + "idParam": 69 + } + }, + { + "id": 14395, + "stationId": 117, + "param": { + "paramName": "pył zawieszony PM10", + "paramFormula": "PM10", + "paramCode": "PM10", + "idParam": 3 + } + } + ] \ No newline at end of file From aecd74c6af49058c48e119be94d658ee380e4852 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Sep 2020 02:35:37 -0500 Subject: [PATCH 593/862] Add service to scan for new Plex clients (#39074) --- homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/services.py | 18 +++++++++++++- homeassistant/components/plex/services.yaml | 3 +++ tests/components/plex/test_services.py | 26 +++++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index b914a89f744..9823443b897 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -46,3 +46,4 @@ MANUAL_SETUP_STRING = "Configure Plex server manually" SERVICE_PLAY_ON_SONOS = "play_on_sonos" SERVICE_REFRESH_LIBRARY = "refresh_library" +SERVICE_SCAN_CLIENTS = "scan_for_clients" diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index cd127c1465e..23dea809a0d 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -4,7 +4,15 @@ import logging from plexapi.exceptions import NotFound import voluptuous as vol -from .const import DOMAIN, SERVERS, SERVICE_REFRESH_LIBRARY +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + DOMAIN, + PLEX_UPDATE_PLATFORMS_SIGNAL, + SERVERS, + SERVICE_REFRESH_LIBRARY, + SERVICE_SCAN_CLIENTS, +) REFRESH_LIBRARY_SCHEMA = vol.Schema( {vol.Optional("server_name"): str, vol.Required("library_name"): str} @@ -19,12 +27,20 @@ async def async_setup_services(hass): async def async_refresh_library_service(service_call): await hass.async_add_executor_job(refresh_library, hass, service_call) + async def async_scan_clients_service(_): + _LOGGER.info("Scanning for new Plex clients") + for server_id in hass.data[DOMAIN][SERVERS]: + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + hass.services.async_register( DOMAIN, SERVICE_REFRESH_LIBRARY, async_refresh_library_service, schema=REFRESH_LIBRARY_SCHEMA, ) + hass.services.async_register( + DOMAIN, SERVICE_SCAN_CLIENTS, async_scan_clients_service + ) return True diff --git a/homeassistant/components/plex/services.yaml b/homeassistant/components/plex/services.yaml index e9fb40939b3..366acb43a5b 100644 --- a/homeassistant/components/plex/services.yaml +++ b/homeassistant/components/plex/services.yaml @@ -21,3 +21,6 @@ refresh_library: library_name: description: Name of the Plex library to refresh. example: "TV Shows" + +scan_for_clients: + description: Scan for available clients from the Plex server(s), local network, and plex.tv. diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index b40c9f6d4d3..078ba3b97e9 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -5,6 +5,7 @@ from homeassistant.components.plex.const import ( DOMAIN, PLEX_SERVER_CONFIG, SERVICE_REFRESH_LIBRARY, + SERVICE_SCAN_CLIENTS, ) from homeassistant.const import ( CONF_HOST, @@ -100,3 +101,28 @@ async def test_refresh_library(hass): True, ) assert not mock_update.called + + +async def test_scan_clients(hass): + """Test scan_for_clients service call.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() + ), 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 await hass.services.async_call( + DOMAIN, + SERVICE_SCAN_CLIENTS, + blocking=True, + ) From f2f68859ccdba237e8cfabdbd62c3ad8e6e59f1c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 3 Sep 2020 09:52:51 +0200 Subject: [PATCH 594/862] Fix wrong error message on adding a new HomematicIP Cloud AP (#39599) --- homeassistant/components/homematicip_cloud/config_flow.py | 3 +++ homeassistant/components/homematicip_cloud/hap.py | 4 ++-- homeassistant/components/homematicip_cloud/strings.json | 2 +- .../components/homematicip_cloud/translations/en.json | 2 +- tests/components/homematicip_cloud/test_hap.py | 4 +--- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 547289f871a..b6b78948894 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -47,6 +47,9 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): _LOGGER.info("Connection to HomematicIP Cloud established") return await self.async_step_link() + _LOGGER.info("Connection to HomematicIP Cloud failed") + errors["base"] = "invalid_sgtin_or_pin" + return self.async_show_form( step_id="init", data_schema=vol.Schema( diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 78f1d57ac93..164997d5582 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -34,7 +34,7 @@ class HomematicipAuth: self.auth = await self.get_auth( self.hass, self.config.get(HMIPC_HAPID), self.config.get(HMIPC_PIN) ) - return True + return self.auth is not None except HmipcConnectionError: return False @@ -63,7 +63,7 @@ class HomematicipAuth: auth.pin = pin await auth.connectionRequest("HomeAssistant") except HmipConnectionError: - return False + return None return auth diff --git a/homeassistant/components/homematicip_cloud/strings.json b/homeassistant/components/homematicip_cloud/strings.json index 2b2a75ebc08..fddb2b85df6 100644 --- a/homeassistant/components/homematicip_cloud/strings.json +++ b/homeassistant/components/homematicip_cloud/strings.json @@ -16,7 +16,7 @@ }, "error": { "register_failed": "Failed to register, please try again.", - "invalid_pin": "Invalid PIN, please try again.", + "invalid_sgtin_or_pin": "Invalid SGTIN or PIN, please try again.", "press_the_button": "Please press the blue button.", "timeout_button": "Blue button press timeout, please try again." }, diff --git a/homeassistant/components/homematicip_cloud/translations/en.json b/homeassistant/components/homematicip_cloud/translations/en.json index 26ca6eb60d6..74d42fc7bc4 100644 --- a/homeassistant/components/homematicip_cloud/translations/en.json +++ b/homeassistant/components/homematicip_cloud/translations/en.json @@ -6,7 +6,7 @@ "unknown": "Unknown error occurred." }, "error": { - "invalid_pin": "Invalid PIN, please try again.", + "invalid_sgtin_or_pin": "Invalid SGTIN or PIN, please try again.", "press_the_button": "Please press the blue button.", "register_failed": "Failed to register, please try again.", "timeout_button": "Blue button press timeout, please try again." diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index ca701622e90..2a4833553d2 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -175,9 +175,7 @@ async def test_auth_create_exception(hass, simple_mock_auth): "homeassistant.components.homematicip_cloud.hap.AsyncAuth", return_value=simple_mock_auth, ): - assert await hmip_auth.async_setup() - await hass.async_block_till_done() - assert not hmip_auth.auth + assert not await hmip_auth.async_setup() with patch( "homeassistant.components.homematicip_cloud.hap.AsyncAuth", From 6d1ba10788aa490e030eba758554274ce093c0a9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Sep 2020 10:43:51 +0200 Subject: [PATCH 595/862] Bump hass-nabucasa to 0.36.0 (#39603) * Bump hass-nabucasa to 0.36.0 * hass-nabucasa 0.36.1 --- homeassistant/components/cloud/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/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index e4ca94d6b38..63afb402b9f 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.35.0"], + "requirements": ["hass-nabucasa==0.36.1"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d88eab47364..5fc91bc5dbc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 -hass-nabucasa==0.35.0 +hass-nabucasa==0.36.1 home-assistant-frontend==20200901.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index 83fd8bef72b..e6d162ec76c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -716,7 +716,7 @@ habitipy==0.2.0 hangups==0.4.10 # homeassistant.components.cloud -hass-nabucasa==0.35.0 +hass-nabucasa==0.36.1 # homeassistant.components.jewish_calendar hdate==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfac71b9814..ba04c1d5d0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ ha-ffmpeg==2.0 hangups==0.4.10 # homeassistant.components.cloud -hass-nabucasa==0.35.0 +hass-nabucasa==0.36.1 # homeassistant.components.jewish_calendar hdate==0.9.5 From 7e50a4999c85d6208f723ac12c876e3501dfd246 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 3 Sep 2020 10:54:25 +0200 Subject: [PATCH 596/862] Add support for Shelly Gas to the Shelly integration (#39478) Co-authored-by: Paulus Schoutsen --- .../components/shelly/binary_sensor.py | 24 ++++++++++++++++++- homeassistant/components/shelly/sensor.py | 11 +++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 52a752a64f9..80e935168f0 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -2,6 +2,7 @@ import aioshelly from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, @@ -15,6 +16,7 @@ from .const import DOMAIN SENSORS = { "dwIsOpened": DEVICE_CLASS_OPENING, "flood": DEVICE_CLASS_MOISTURE, + "gas": DEVICE_CLASS_GAS, "overpower": None, "overtemp": None, "smoke": DEVICE_CLASS_SMOKE, @@ -66,10 +68,30 @@ class ShellySensor(ShellyBlockEntity, BinarySensorEntity): @property def is_on(self): - """Return true if sensor state is 1.""" + """Return true if sensor state is on.""" + if self.attribute == "gas": + # Gas sensor value of Shelly Gas can be none/mild/heavy/test. We return True + # when the value is mild or heavy. + return getattr(self.block, self.attribute) in ["mild", "heavy"] return bool(getattr(self.block, self.attribute)) @property def device_class(self): """Device class of sensor.""" return self._device_class + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.attribute == "gas": + # We return raw value of the gas sensor as an attribute. + return {"detected": getattr(self.block, self.attribute)} + + @property + def available(self): + """Available.""" + if self.attribute == "gas": + # "sensorOp" is "normal" when Shelly Gas is working properly and taking + # measurements. + return super().available and self.block.sensorOp == "normal" + return super().available diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 953499762a0..cfeaea98357 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -3,6 +3,7 @@ import aioshelly from homeassistant.components import sensor from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, POWER_WATT, @@ -18,6 +19,7 @@ from .const import DOMAIN SENSORS = { "battery": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_BATTERY], + "concentration": [CONCENTRATION_PARTS_PER_MILLION, None], "current": [ELECTRICAL_CURRENT_AMPERE, sensor.DEVICE_CLASS_CURRENT], "deviceTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], "energy": [ENERGY_KILO_WATT_HOUR, sensor.DEVICE_CLASS_ENERGY], @@ -117,3 +119,12 @@ class ShellySensor(ShellyBlockEntity, Entity): def device_class(self): """Device class of sensor.""" return self._device_class + + @property + def available(self): + """Available.""" + if self.attribute == "concentration": + # "sensorOp" is "normal" when the Shelly Gas is working properly and taking + # measurements. + return super().available and self.block.sensorOp == "normal" + return super().available From 0cba7acf5a54dadb9a6c9665c3deaa5325caf120 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Sep 2020 13:29:17 +0200 Subject: [PATCH 597/862] Upgrade sentry-sdk to 0.17.3 (#39607) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 66894bfb2e3..471a349a4df 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,6 +3,6 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==0.17.1"], + "requirements": ["sentry-sdk==0.17.3"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index e6d162ec76c..dd8a8376943 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1959,7 +1959,7 @@ sense-hat==2.2.0 sense_energy==0.7.2 # homeassistant.components.sentry -sentry-sdk==0.17.1 +sentry-sdk==0.17.3 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba04c1d5d0f..0cc9335206e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -908,7 +908,7 @@ samsungtvws[websocket]==1.4.0 sense_energy==0.7.2 # homeassistant.components.sentry -sentry-sdk==0.17.1 +sentry-sdk==0.17.3 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 1c90bdddb4a633cfc709611a7b5a782a527c739d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Sep 2020 13:30:42 +0200 Subject: [PATCH 598/862] Upgrade apprise to 0.8.8 (#39606) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 31b89e87b81..beb3a80ceeb 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,6 +2,6 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==0.8.7"], + "requirements": ["apprise==0.8.8"], "codeowners": ["@caronc"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd8a8376943..584c6805eb4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -263,7 +263,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.7 +apprise==0.8.8 # homeassistant.components.aprs aprslib==0.6.46 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0cc9335206e..ccda38fb51c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ androidtv[async]==0.0.49 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.7 +apprise==0.8.8 # homeassistant.components.aprs aprslib==0.6.46 From 8ca972425820534c8fc34ea646d8d7beeb28d87b Mon Sep 17 00:00:00 2001 From: Martin Eberhardt Date: Thu, 3 Sep 2020 13:43:35 +0200 Subject: [PATCH 599/862] Expose more attributes in rejseplanen (#37216) --- .../components/rejseplanen/sensor.py | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 8fdd1f2f858..30d57a3d9dc 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -24,8 +24,12 @@ ATTR_STOP_NAME = "stop" ATTR_ROUTE = "route" ATTR_TYPE = "type" ATTR_DIRECTION = "direction" +ATTR_FINAL_STOP = "final_stop" ATTR_DUE_IN = "due_in" ATTR_DUE_AT = "due_at" +ATTR_SCHEDULED_AT = "scheduled_at" +ATTR_REAL_TIME_AT = "real_time_at" +ATTR_TRACK = "track" ATTR_NEXT_UP = "next_departures" ATTRIBUTION = "Data provided by rejseplanen.dk" @@ -115,18 +119,17 @@ class RejseplanenTransportSensor(Entity): if len(self._times) > 1: next_up = self._times[1:] - return { - ATTR_DUE_IN: self._times[0][ATTR_DUE_IN], - ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], - ATTR_TYPE: self._times[0][ATTR_TYPE], - ATTR_ROUTE: self._times[0][ATTR_ROUTE], - ATTR_DIRECTION: self._times[0][ATTR_DIRECTION], - ATTR_STOP_NAME: self._times[0][ATTR_STOP_NAME], - ATTR_STOP_ID: self._stop_id, + attributes = { ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NEXT_UP: next_up, + ATTR_STOP_ID: self._stop_id, } + if self._times[0] is not None: + attributes.update(self._times[0]) + + return attributes + @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" @@ -203,13 +206,15 @@ class PublicTransportData: for item in results: route = item.get("name") - due_at_date = item.get("rtDate") - due_at_time = item.get("rtTime") + scheduled_date = item.get("date") + scheduled_time = item.get("time") + real_time_date = due_at_date = item.get("rtDate") + real_time_time = due_at_time = item.get("rtTime") if due_at_date is None: - due_at_date = item.get("date") # Scheduled date + due_at_date = scheduled_date if due_at_time is None: - due_at_time = item.get("time") # Scheduled time + due_at_time = scheduled_time if ( due_at_date is not None @@ -217,15 +222,26 @@ class PublicTransportData: and route is not None ): due_at = f"{due_at_date} {due_at_time}" + scheduled_at = f"{scheduled_date} {scheduled_time}" departure_data = { + ATTR_DIRECTION: item.get("direction"), ATTR_DUE_IN: due_in_minutes(due_at), ATTR_DUE_AT: due_at, - ATTR_TYPE: item.get("type"), + ATTR_FINAL_STOP: item.get("finalStop"), ATTR_ROUTE: route, - ATTR_DIRECTION: item.get("direction"), + ATTR_SCHEDULED_AT: scheduled_at, ATTR_STOP_NAME: item.get("stop"), + ATTR_TYPE: item.get("type"), } + + if real_time_date is not None and real_time_time is not None: + departure_data[ + ATTR_REAL_TIME_AT + ] = f"{real_time_date} {real_time_time}" + if item.get("rtTrack") is not None: + departure_data[ATTR_TRACK] = item.get("rtTrack") + self.info.append(departure_data) if not self.info: From 948ec80b6ec27073aa1c644a49aae3dc58f04bc3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Sep 2020 14:47:14 +0200 Subject: [PATCH 600/862] Bump pytradfri to 7.0.0, support multiple gateways (#39609) Co-authored-by: Martin Hjelmare --- homeassistant/components/tradfri/__init__.py | 33 ++++++++++++++++--- .../components/tradfri/config_flow.py | 14 ++++---- homeassistant/components/tradfri/const.py | 2 +- .../components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 41 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 7074532e097..af63fe192cd 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,4 +1,5 @@ """Support for IKEA Tradfri.""" +import asyncio import logging from pytradfri import Gateway, RequestError @@ -27,11 +28,13 @@ from .const import ( DOMAIN, KEY_API, KEY_GATEWAY, - TRADFRI_DEVICE_TYPES, + PLATFORMS, ) _LOGGER = logging.getLogger(__name__) +FACTORY = "tradfri_factory" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -96,7 +99,7 @@ async def async_setup_entry(hass, entry): """Create a gateway.""" # host, identity, key, allow_tradfri_groups - factory = APIFactory( + factory = await APIFactory.init( entry.data[CONF_HOST], psk_id=entry.data[CONF_IDENTITY], psk=entry.data[CONF_KEY], @@ -119,6 +122,8 @@ async def async_setup_entry(hass, entry): hass.data.setdefault(KEY_API, {})[entry.entry_id] = api hass.data.setdefault(KEY_GATEWAY, {})[entry.entry_id] = gateway + tradfri_data = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} + tradfri_data[FACTORY] = factory dev_reg = await hass.helpers.device_registry.async_get_registry() dev_reg.async_get_or_create( @@ -132,9 +137,29 @@ async def async_setup_entry(hass, entry): sw_version=gateway_info.firmware_version, ) - for device in TRADFRI_DEVICE_TYPES: + for component in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, device) + hass.config_entries.async_forward_entry_setup(entry, component) ) return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[KEY_API].pop(entry.entry_id) + hass.data[KEY_GATEWAY].pop(entry.entry_id) + tradfri_data = hass.data[DOMAIN].pop(entry.entry_id) + factory = tradfri_data[FACTORY] + await factory.shutdown() + + return unload_ok diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index e72291c8d88..7947c3ad6de 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -90,7 +90,7 @@ class FlowHandler(config_entries.ConfigFlow): host = discovery_info["host"] for entry in self._async_current_entries(): - if entry.data[CONF_HOST] != host: + if entry.data.get(CONF_HOST) != host: continue # Backwards compat, we update old entries @@ -107,7 +107,7 @@ class FlowHandler(config_entries.ConfigFlow): async def async_step_import(self, user_input): """Import a config entry.""" for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == user_input["host"]: + if entry.data.get(CONF_HOST) == user_input["host"]: return self.async_abort(reason="already_configured") # Happens if user has host directly in configuration.yaml @@ -141,8 +141,8 @@ class FlowHandler(config_entries.ConfigFlow): same_hub_entries = [ entry.entry_id for entry in self._async_current_entries() - if entry.data[CONF_GATEWAY_ID] == gateway_id - or entry.data[CONF_HOST] == host + if entry.data.get(CONF_GATEWAY_ID) == gateway_id + or entry.data.get(CONF_HOST) == host ] if same_hub_entries: @@ -161,7 +161,7 @@ async def authenticate(hass, host, security_code): identity = uuid4().hex - api_factory = APIFactory(host, psk_id=identity) + api_factory = await APIFactory.init(host, psk_id=identity) try: with async_timeout.timeout(5): @@ -170,6 +170,8 @@ async def authenticate(hass, host, security_code): raise AuthError("invalid_security_code") from err except asyncio.TimeoutError as err: raise AuthError("timeout") from err + finally: + await api_factory.shutdown() return await get_gateway_info(hass, host, identity, key) @@ -178,7 +180,7 @@ async def get_gateway_info(hass, host, identity, key): """Return info for the gateway.""" try: - factory = APIFactory(host, psk_id=identity, psk=key) + factory = await APIFactory.init(host, psk_id=identity, psk=key) api = factory.request gateway = Gateway() diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index ffb5d64f6d7..423620ecb2b 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -23,4 +23,4 @@ KEY_GATEWAY = "tradfri_gateway" KEY_SECURITY_CODE = "security_code" SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION -TRADFRI_DEVICE_TYPES = ["cover", "light", "sensor", "switch"] +PLATFORMS = ["cover", "light", "sensor", "switch"] diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 12457975eee..05cfdcdfeee 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==6.4.0"], + "requirements": ["pytradfri[async]==7.0.0"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 584c6805eb4..4bca4967ed7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1805,7 +1805,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==6.4.0 +pytradfri[async]==7.0.0 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccda38fb51c..4f767ff8552 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==6.4.0 +pytradfri[async]==7.0.0 # homeassistant.components.vera pyvera==0.3.9 From 77c6a48553e83e14ff87e9983b1daf86fb5fb54f Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Thu, 3 Sep 2020 17:58:31 +0300 Subject: [PATCH 601/862] Update aioswitcher to 1.2.1 (#39614) --- homeassistant/components/switcher_kis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index a4e312908f2..c0cf7f18de6 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,5 +3,5 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi"], - "requirements": ["aioswitcher==1.2.0"] + "requirements": ["aioswitcher==1.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4bca4967ed7..8a9e3748931 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ aiopylgtv==0.3.3 aioshelly==0.2.1 # homeassistant.components.switcher_kis -aioswitcher==1.2.0 +aioswitcher==1.2.1 # homeassistant.components.unifi aiounifi==23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f767ff8552..1ec4d5a09db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aiopylgtv==0.3.3 aioshelly==0.2.1 # homeassistant.components.switcher_kis -aioswitcher==1.2.0 +aioswitcher==1.2.1 # homeassistant.components.unifi aiounifi==23 From 4dee2b599a789a10a84b9ff4eef83341f3fcd379 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 3 Sep 2020 17:23:42 +0200 Subject: [PATCH 602/862] Add HmIP-STV to HomematicIP Cloud (#39518) * add general attribute for connection type * Add HmIP-STV to HomematicIP Cloud --- .../homematicip_cloud/binary_sensor.py | 15 ++++++-- .../homematicip_cloud/generic_entity.py | 2 ++ .../homematicip_cloud/test_binary_sensor.py | 36 +++++++++++++++++++ .../homematicip_cloud/test_device.py | 2 +- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 15ba9049dba..50e8360675b 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -16,6 +16,7 @@ from homematicip.aio.device import ( AsyncShutterContact, AsyncShutterContactMagnetic, AsyncSmokeDetector, + AsyncTiltVibrationSensor, AsyncWaterSensor, AsyncWeatherSensor, AsyncWeatherSensorPlus, @@ -85,6 +86,8 @@ async def async_setup_entry( for device in hap.home.devices: if isinstance(device, AsyncAccelerationSensor): entities.append(HomematicipAccelerationSensor(hap, device)) + if isinstance(device, AsyncTiltVibrationSensor): + entities.append(HomematicipTiltVibrationSensor(hap, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): entities.append(HomematicipContactInterface(hap, device)) if isinstance( @@ -133,8 +136,8 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipAccelerationSensor(HomematicipGenericEntity, BinarySensorEntity): - """Representation of the HomematicIP acceleration sensor.""" +class HomematicipBaseActionSensor(HomematicipGenericEntity, BinarySensorEntity): + """Representation of the HomematicIP base action sensor.""" @property def device_class(self) -> str: @@ -159,6 +162,14 @@ class HomematicipAccelerationSensor(HomematicipGenericEntity, BinarySensorEntity return state_attr +class HomematicipAccelerationSensor(HomematicipBaseActionSensor): + """Representation of the HomematicIP acceleration sensor.""" + + +class HomematicipTiltVibrationSensor(HomematicipBaseActionSensor): + """Representation of the HomematicIP tilt vibration sensor.""" + + class HomematicipContactInterface(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP contact interface.""" diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 7450a82943f..3a19c1b2afe 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -17,6 +17,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" ATTR_LOW_BATTERY = "low_battery" ATTR_CONFIG_PENDING = "config_pending" +ATTR_CONNECTION_TYPE = "connection_type" ATTR_DUTY_CYCLE_REACHED = "duty_cycle_reached" ATTR_ID = "id" ATTR_IS_GROUP = "is_group" @@ -43,6 +44,7 @@ DEVICE_ATTRIBUTE_ICONS = { DEVICE_ATTRIBUTES = { "modelType": ATTR_MODEL_TYPE, + "connectionType": ATTR_CONNECTION_TYPE, "sabotage": ATTR_SABOTAGE, "dutyCycle": ATTR_DUTY_CYCLE_REACHED, "rssiDeviceValue": ATTR_RSSI_DEVICE, diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 06b19ae0805..f15b2b56a95 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -75,6 +75,42 @@ async def test_hmip_acceleration_sensor(hass, default_mock_hap_factory): assert len(hmip_device.mock_calls) == service_call_counter + 2 +async def test_hmip_tilt_vibration_sensor(hass, default_mock_hap_factory): + """Test HomematicipTiltVibrationSensor.""" + entity_id = "binary_sensor.garage_neigungs_und_erschutterungssensor" + entity_name = "Garage Neigungs- und Erschütterungssensor" + device_model = "HmIP-STV" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_MODE] == "FLAT_DECT" + assert ( + ha_state.attributes[ATTR_ACCELERATION_SENSOR_SENSITIVITY] == "SENSOR_RANGE_2G" + ) + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE] == 45 + service_call_counter = len(hmip_device.mock_calls) + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", False + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert len(hmip_device.mock_calls) == service_call_counter + 1 + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", True + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert len(hmip_device.mock_calls) == service_call_counter + 2 + + async def test_hmip_contact_interface(hass, default_mock_hap_factory): """Test HomematicipContactInterface.""" entity_id = "binary_sensor.kontakt_schnittstelle_unterputz_1_fach" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 0154eb7b327..f7999b5f015 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 190 + assert len(mock_hap.hmip_device_by_entity_id) == 191 async def test_hmip_remove_device(hass, default_mock_hap_factory): From e288246366659790a0960bf16404b78fa645597d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 3 Sep 2020 17:26:13 +0200 Subject: [PATCH 603/862] Add troubleshooting link to error message for xiaomi_aqara (#39617) --- homeassistant/components/xiaomi_aqara/strings.json | 2 +- homeassistant/components/xiaomi_aqara/translations/en.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index 5cbdc91a661..bc50c491866 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -31,7 +31,7 @@ "discovery_error": "Failed to discover a Xiaomi Aqara Gateway, try using the IP of the device running HomeAssistant as interface", "invalid_interface": "Invalid network interface", "invalid_key": "Invalid gateway key", - "invalid_host": "Invalid [%key:common::config_flow::data::ip%]", + "invalid_host": "Invalid [%key:common::config_flow::data::ip%], see https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_mac": "Invalid Mac Address" }, "abort": { diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json index 25d44d80b54..fe77ae51a63 100644 --- a/homeassistant/components/xiaomi_aqara/translations/en.json +++ b/homeassistant/components/xiaomi_aqara/translations/en.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "Failed to discover a Xiaomi Aqara Gateway, try using the IP of the device running HomeAssistant as interface", - "invalid_host": "Invalid IP Address", + "invalid_host": "Invalid IP Address, see https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Invalid network interface", "invalid_key": "Invalid gateway key", "invalid_mac": "Invalid Mac Address", @@ -41,4 +41,4 @@ } } } -} \ No newline at end of file +} From 4ebcbadfc2b65725e078398ee30f1f3c5fc6db54 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Sep 2020 11:10:37 -0500 Subject: [PATCH 604/862] Address Plex review comments (#39591) --- homeassistant/components/plex/__init__.py | 2 +- homeassistant/components/plex/services.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 15212175853..4e5abad4f79 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -239,7 +239,7 @@ def play_on_sonos(hass, service_call): _LOGGER.error( "Requested Plex server '%s' not found in %s", plex_server_name, - list(map(lambda x: x.friendly_name, plex_servers)), + [x.friendly_name for x in plex_servers], ) return else: diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 23dea809a0d..857f6fcf54e 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -48,7 +48,7 @@ async def async_setup_services(hass): def refresh_library(hass, service_call): """Scan a Plex library for new and updated media.""" plex_server_name = service_call.data.get("server_name") - library_name = service_call.data.get("library_name") + library_name = service_call.data["library_name"] plex_server = get_plex_server(hass, plex_server_name) if not plex_server: @@ -60,11 +60,11 @@ def refresh_library(hass, service_call): _LOGGER.error( "Library with name '%s' not found in %s", library_name, - list(map(lambda x: x.title, plex_server.library.sections())), + [x.title for x in plex_server.library.sections()], ) return - _LOGGER.info("Scanning %s for new and updated media", library_name) + _LOGGER.debug("Scanning %s for new and updated media", library_name) library.update() @@ -78,7 +78,7 @@ def get_plex_server(hass, plex_server_name=None): _LOGGER.error( "Requested Plex server '%s' not found in %s", plex_server_name, - list(map(lambda x: x.friendly_name, plex_servers)), + [x.friendly_name for x in plex_servers], ) return None elif len(plex_servers) == 1: @@ -86,6 +86,6 @@ def get_plex_server(hass, plex_server_name=None): _LOGGER.error( "Multiple Plex servers configured and no selection made: %s", - list(map(lambda x: x.friendly_name, plex_servers)), + [x.friendly_name for x in plex_servers], ) return None From fbbfd46fb831e06b96adef91edfed35295ae98fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Sep 2020 18:13:33 +0200 Subject: [PATCH 605/862] Add .well-known/password-change (#39613) --- homeassistant/components/frontend/__init__.py | 4 ++++ homeassistant/components/http/__init__.py | 4 ++-- tests/components/frontend/test_init.py | 9 +++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 90089d50340..2a741afcfbc 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -284,6 +284,10 @@ async def async_setup(hass, config): hass.http.register_static_path( "/auth/authorize", str(root_path / "authorize.html"), False ) + # https://wicg.github.io/change-password-url/ + hass.http.register_redirect( + "/.well-known/change-password", "/profile", redirect_exc=web.HTTPFound + ) local = hass.config.path("www") if os.path.isdir(local): diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index cb0ecec8a2b..421b17e47cc 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -345,7 +345,7 @@ class HomeAssistantHTTP: view.register(self.app, self.app.router) - def register_redirect(self, url, redirect_to): + def register_redirect(self, url, redirect_to, *, redirect_exc=HTTPMovedPermanently): """Register a redirect with the server. If given this must be either a string or callable. In case of a @@ -357,7 +357,7 @@ class HomeAssistantHTTP: async def redirect(request): """Redirect to location.""" - raise HTTPMovedPermanently(redirect_to) + raise redirect_exc(redirect_to) self.app.router.add_route("GET", url, redirect) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 5e6bbe8b2d4..a7ecbb0e5fe 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -484,3 +484,12 @@ async def test_get_version(hass, hass_ws_client): assert msg["type"] == TYPE_RESULT assert msg["success"] assert msg["result"] == {"version": cur_version} + + +async def test_static_paths(hass, mock_http_client): + """Test static paths.""" + resp = await mock_http_client.get( + "/.well-known/change-password", allow_redirects=False + ) + assert resp.status == 302 + assert resp.headers["location"] == "/profile" From 9baa7c6c24dcb3f4a3750469777cb72b2cb17766 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Thu, 3 Sep 2020 09:22:00 -0700 Subject: [PATCH 606/862] Restart keepalive streams (#38863) --- homeassistant/components/stream/__init__.py | 4 ++ homeassistant/components/stream/worker.py | 41 +++++++++++++++++---- tests/components/stream/test_hls.py | 36 ++++++++++++++++++ 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 476b64b9650..3ce3d0af6fd 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -171,6 +171,10 @@ class Stream: from .worker import stream_worker if self._thread is None or not self._thread.isAlive(): + if self._thread is not None: + # The thread must have crashed/exited. Join to clean up the + # previous thread. + self._thread.join(timeout=0) self._thread_quit = threading.Event() self._thread = threading.Thread( name="stream_worker", diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index ea4359e2a0c..b461d17cf1c 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -2,6 +2,7 @@ from collections import deque import io import logging +import time import av @@ -35,6 +36,25 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): def stream_worker(hass, stream, quit_event): + """Handle consuming streams and restart keepalive streams.""" + + wait_timeout = 0 + while not quit_event.wait(timeout=wait_timeout): + start_time = time.time() + try: + _stream_worker_internal(hass, stream, quit_event) + except av.error.FFmpegError: # pylint: disable=c-extension-no-member + _LOGGER.exception("Stream connection failed: %s", stream.source) + if not stream.keepalive or quit_event.is_set(): + break + # To avoid excessive restarts, don't restart faster than once every 40 seconds. + wait_timeout = max(40 - (time.time() - start_time), 0) + _LOGGER.debug( + "Restarting stream worker in %d seconds: %s", wait_timeout, stream.source, + ) + + +def _stream_worker_internal(hass, stream, quit_event): """Handle consuming streams.""" container = av.open(stream.source, options=stream.options) @@ -112,13 +132,15 @@ def stream_worker(hass, stream, quit_event): audio_stream = None except (av.AVError, StopIteration) as ex: - # End of stream, clear listeners and stop thread - for fmt, _ in outputs.items(): - hass.loop.call_soon_threadsafe(stream.outputs[fmt].put, None) + if not stream.keepalive: + # End of stream, clear listeners and stop thread + for fmt, _ in outputs.items(): + hass.loop.call_soon_threadsafe(stream.outputs[fmt].put, None) _LOGGER.error( "Error demuxing stream while finding first packet: %s", str(ex) ) - quit_event.set() + return False + return True def initialize_segment(video_pts): """Reset some variables and initialize outputs for each segment.""" @@ -159,7 +181,9 @@ def stream_worker(hass, stream, quit_event): packet.stream = output_streams[audio_stream] buffer.output.mux(packet) - peek_first_pts() + if not peek_first_pts(): + container.close() + return last_dts = {k: v - 1 for k, v in first_pts.items()} initialize_segment(first_pts[video_stream]) @@ -179,9 +203,10 @@ def stream_worker(hass, stream, quit_event): continue last_packet_was_without_dts = False except (av.AVError, StopIteration) as ex: - # End of stream, clear listeners and stop thread - for fmt, _ in outputs.items(): - hass.loop.call_soon_threadsafe(stream.outputs[fmt].put, None) + if not stream.keepalive: + # End of stream, clear listeners and stop thread + for fmt, _ in outputs.items(): + hass.loop.call_soon_threadsafe(stream.outputs[fmt].put, None) _LOGGER.error("Error demuxing stream: %s", str(ex)) break diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index b4c0b0e536f..863513c8157 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -2,6 +2,7 @@ from datetime import timedelta from urllib.parse import urlparse +import av import pytest from homeassistant.components.stream import request_stream @@ -9,6 +10,7 @@ from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video, preload_stream @@ -122,3 +124,37 @@ async def test_stream_ended(hass): # Stop stream, if it hasn't quit already stream.stop() + + +async def test_stream_keepalive(hass): + """Test hls stream retries the stream when keepalive=True.""" + await async_setup_component(hass, "stream", {"stream": {}}) + + # Setup demo HLS track + source = "test_stream_keepalive_source" + stream = preload_stream(hass, source) + track = stream.add_provider("hls") + track.num_segments = 2 + + cur_time = 0 + + def time_side_effect(): + nonlocal cur_time + if cur_time >= 80: + stream.keepalive = False # Thread should exit and be joinable. + cur_time += 40 + return cur_time + + with patch("av.open") as av_open, patch( + "homeassistant.components.stream.worker.time" + ) as mock_time: + av_open.side_effect = av.error.InvalidDataError(-2, "error") + mock_time.time.side_effect = time_side_effect + # Request stream + request_stream(hass, source, keepalive=True) + stream._thread.join() + stream._thread = None + assert av_open.call_count == 2 + + # Stop stream, if it hasn't quit already + stream.stop() From 8818a5ab6c6f7ff6f45e2dc99973457eadec24a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20W=C4=99grzynek?= Date: Thu, 3 Sep 2020 18:25:30 +0200 Subject: [PATCH 607/862] Use DataUpdateCoordinator for supla (#38921) * Linter suggestions * Store coordinator in hass.data[supla_coordinators] * Server cleanup * Spelling mistake * Fixes suggested in review * Pass server and coordinator during async_setup_platform * Linter changes * Rename fetch_channels to _fetch_channels * Linter suggestions * Store coordinator in hass.data[supla_coordinators] * Server cleanup * Fixes suggested in review * Pass server and coordinator during async_setup_platform * Linter changes * Remove scan interval configuration option * Linting * Isort * Disable polling, update asyncpysupla version * Black fixes * Update manifest.json Co-authored-by: Chris Talkington --- homeassistant/components/supla/__init__.py | 123 ++++++++++++++----- homeassistant/components/supla/cover.py | 65 ++++++---- homeassistant/components/supla/manifest.json | 2 +- homeassistant/components/supla/switch.py | 31 +++-- requirements_all.txt | 6 +- 5 files changed, 161 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 6c9bfb8d16e..8912ee8a59b 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -1,32 +1,41 @@ """Support for Supla devices.""" +from datetime import timedelta import logging from typing import Optional -from pysupla import SuplaAPI +import async_timeout +from asyncpysupla import SuplaAPI import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -DOMAIN = "supla" +DOMAIN = "supla" CONF_SERVER = "server" CONF_SERVERS = "servers" +SCAN_INTERVAL = timedelta(seconds=10) + SUPLA_FUNCTION_HA_CMP_MAP = { "CONTROLLINGTHEROLLERSHUTTER": "cover", "CONTROLLINGTHEGATE": "cover", "LIGHTSWITCH": "switch", } SUPLA_FUNCTION_NONE = "NONE" -SUPLA_CHANNELS = "supla_channels" SUPLA_SERVERS = "supla_servers" +SUPLA_COORDINATORS = "supla_coordinators" SERVER_CONFIG = vol.Schema( - {vol.Required(CONF_SERVER): cv.string, vol.Required(CONF_ACCESS_TOKEN): cv.string} + { + vol.Required(CONF_SERVER): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, + } ) CONFIG_SCHEMA = vol.Schema( @@ -39,25 +48,27 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, base_config): +async def async_setup(hass, base_config): """Set up the Supla component.""" server_confs = base_config[DOMAIN][CONF_SERVERS] - hass.data[SUPLA_SERVERS] = {} - hass.data[SUPLA_CHANNELS] = {} + hass.data[DOMAIN] = {SUPLA_SERVERS: {}, SUPLA_COORDINATORS: {}} + + session = async_get_clientsession(hass) for server_conf in server_confs: server_address = server_conf[CONF_SERVER] - server = SuplaAPI(server_address, server_conf[CONF_ACCESS_TOKEN]) + server = SuplaAPI(server_address, server_conf[CONF_ACCESS_TOKEN], session) # Test connection try: - srv_info = server.get_server_info() + srv_info = await server.get_server_info() if srv_info.get("authenticated"): - hass.data[SUPLA_SERVERS][server_conf[CONF_SERVER]] = server + hass.data[DOMAIN][SUPLA_SERVERS][server_conf[CONF_SERVER]] = server + else: _LOGGER.error( "Server: %s not configured. API call returned: %s", @@ -71,23 +82,46 @@ def setup(hass, base_config): ) return False - discover_devices(hass, base_config) + await discover_devices(hass, base_config) return True -def discover_devices(hass, hass_config): +async def discover_devices(hass, hass_config): """ Run periodically to discover new devices. - Currently it's only run at startup. + Currently it is only run at startup. """ component_configs = {} - for server_name, server in hass.data[SUPLA_SERVERS].items(): + for server_name, server in hass.data[DOMAIN][SUPLA_SERVERS].items(): - for channel in server.get_channels(include=["iodevice"]): + async def _fetch_channels(): + async with async_timeout.timeout(SCAN_INTERVAL.total_seconds()): + channels = { + channel["id"]: channel + for channel in await server.get_channels( + include=["iodevice", "state", "connected"] + ) + } + return channels + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{DOMAIN}-{server_name}", + update_method=_fetch_channels, + update_interval=SCAN_INTERVAL, + ) + + await coordinator.async_refresh() + + hass.data[DOMAIN][SUPLA_COORDINATORS][server_name] = coordinator + + for channel_id, channel in coordinator.data.items(): channel_function = channel["function"]["name"] + if channel_function == SUPLA_FUNCTION_NONE: _LOGGER.debug( "Ignored function: %s, channel id: %s", @@ -107,25 +141,38 @@ def discover_devices(hass, hass_config): continue channel["server_name"] = server_name - component_configs.setdefault(component_name, []).append(channel) + component_configs.setdefault(component_name, []).append( + { + "channel_id": channel_id, + "server_name": server_name, + "function_name": channel["function"]["name"], + } + ) # Load discovered devices - for component_name, channel in component_configs.items(): - load_platform(hass, component_name, "supla", channel, hass_config) + for component_name, config in component_configs.items(): + await async_load_platform(hass, component_name, DOMAIN, config, hass_config) class SuplaChannel(Entity): """Base class of a Supla Channel (an equivalent of HA's Entity).""" - def __init__(self, channel_data): - """Channel data -- raw channel information from PySupla.""" - self.server_name = channel_data["server_name"] - self.channel_data = channel_data + def __init__(self, config, server, coordinator): + """Init from config, hookup[ server and coordinator.""" + self.server_name = config["server_name"] + self.channel_id = config["channel_id"] + self.server = server + self.coordinator = coordinator @property - def server(self): - """Return PySupla's server component associated with entity.""" - return self.hass.data[SUPLA_SERVERS][self.server_name] + def channel_data(self): + """Return channel data taken from coordinator.""" + return self.coordinator.data.get(self.channel_id) + + @property + def should_poll(self): + """Supla uses DataUpdateCoordinator, so no additional polling needed.""" + return False @property def unique_id(self) -> str: @@ -150,7 +197,13 @@ class SuplaChannel(Entity): return False return state.get("connected") - def action(self, action, **add_pars): + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_action(self, action, **add_pars): """ Run server action. @@ -163,10 +216,14 @@ class SuplaChannel(Entity): self.channel_data["id"], add_pars, ) - self.server.execute_action(self.channel_data["id"], action, **add_pars) + await self.server.execute_action(self.channel_data["id"], action, **add_pars) - def update(self): - """Call to update state.""" - self.channel_data = self.server.get_channel( - self.channel_data["id"], include=["connected", "state"] - ) + # Update state + await self.coordinator.async_request_refresh() + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index 1c0f2f60431..ac71bc4ea8f 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -7,7 +7,12 @@ from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, CoverEntity, ) -from homeassistant.components.supla import SuplaChannel +from homeassistant.components.supla import ( + DOMAIN, + SUPLA_COORDINATORS, + SUPLA_SERVERS, + SuplaChannel, +) _LOGGER = logging.getLogger(__name__) @@ -15,7 +20,7 @@ SUPLA_SHUTTER = "CONTROLLINGTHEROLLERSHUTTER" SUPLA_GATE = "CONTROLLINGTHEGATE" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Supla covers.""" if discovery_info is None: return @@ -24,12 +29,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [] for device in discovery_info: - device_name = device["function"]["name"] + device_name = device["function_name"] + server_name = device["server_name"] + if device_name == SUPLA_SHUTTER: - entities.append(SuplaCover(device)) + entities.append( + SuplaCover( + device, + hass.data[DOMAIN][SUPLA_SERVERS][server_name], + hass.data[DOMAIN][SUPLA_COORDINATORS][server_name], + ) + ) + elif device_name == SUPLA_GATE: - entities.append(SuplaGateDoor(device)) - add_entities(entities) + entities.append( + SuplaGateDoor( + device, + hass.data[DOMAIN][SUPLA_SERVERS][server_name], + hass.data[DOMAIN][SUPLA_COORDINATORS][server_name], + ) + ) + + async_add_entities(entities) class SuplaCover(SuplaChannel, CoverEntity): @@ -43,9 +64,9 @@ class SuplaCover(SuplaChannel, CoverEntity): return 100 - state["shut"] return None - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - self.action("REVEAL", percentage=kwargs.get(ATTR_POSITION)) + await self.async_action("REVEAL", percentage=kwargs.get(ATTR_POSITION)) @property def is_closed(self): @@ -54,17 +75,17 @@ class SuplaCover(SuplaChannel, CoverEntity): return None return self.current_cover_position == 0 - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the cover.""" - self.action("REVEAL") + await self.async_action("REVEAL") - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close the cover.""" - self.action("SHUT") + await self.async_action("SHUT") - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the cover.""" - self.action("STOP") + await self.async_action("STOP") class SuplaGateDoor(SuplaChannel, CoverEntity): @@ -78,23 +99,23 @@ class SuplaGateDoor(SuplaChannel, CoverEntity): return state.get("hi") return None - def open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs) -> None: """Open the gate.""" if self.is_closed: - self.action("OPEN_CLOSE") + await self.async_action("OPEN_CLOSE") - def close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs) -> None: """Close the gate.""" if not self.is_closed: - self.action("OPEN_CLOSE") + await self.async_action("OPEN_CLOSE") - def stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs) -> None: """Stop the gate.""" - self.action("OPEN_CLOSE") + await self.async_action("OPEN_CLOSE") - def toggle(self, **kwargs) -> None: + async def async_toggle(self, **kwargs) -> None: """Toggle the gate.""" - self.action("OPEN_CLOSE") + await self.async_action("OPEN_CLOSE") @property def device_class(self): diff --git a/homeassistant/components/supla/manifest.json b/homeassistant/components/supla/manifest.json index a4ab0e72719..1a2dcf3cbc5 100644 --- a/homeassistant/components/supla/manifest.json +++ b/homeassistant/components/supla/manifest.json @@ -2,6 +2,6 @@ "domain": "supla", "name": "Supla", "documentation": "https://www.home-assistant.io/integrations/supla", - "requirements": ["pysupla==0.0.3"], + "requirements": ["asyncpysupla==0.0.5"], "codeowners": ["@mwegrzynek"] } diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index 61f218b75d9..9122d9d1970 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -2,32 +2,49 @@ import logging from pprint import pformat -from homeassistant.components.supla import SuplaChannel +from homeassistant.components.supla import ( + DOMAIN, + SUPLA_COORDINATORS, + SUPLA_SERVERS, + SuplaChannel, +) from homeassistant.components.switch import SwitchEntity _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Supla switches.""" if discovery_info is None: return _LOGGER.debug("Discovery: %s", pformat(discovery_info)) - add_entities([SuplaSwitch(device) for device in discovery_info]) + entities = [] + for device in discovery_info: + server_name = device["server_name"] + + entities.append( + SuplaSwitch( + device, + hass.data[DOMAIN][SUPLA_SERVERS][server_name], + hass.data[DOMAIN][SUPLA_COORDINATORS][server_name], + ) + ) + + async_add_entities(entities) class SuplaSwitch(SuplaChannel, SwitchEntity): """Representation of a Supla Switch.""" - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn on the switch.""" - self.action("TURN_ON") + await self.async_action("TURN_ON") - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn off the switch.""" - self.action("TURN_OFF") + await self.async_action("TURN_OFF") @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 8a9e3748931..ff53d0e6dd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -287,6 +287,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp async-upnp-client==0.14.13 +# homeassistant.components.supla +asyncpysupla==0.0.5 + # homeassistant.components.aten_pe atenpdu==0.3.0 @@ -1660,9 +1663,6 @@ pystiebeleltron==0.0.1.dev2 # homeassistant.components.suez_water pysuez==0.1.19 -# homeassistant.components.supla -pysupla==0.0.3 - # homeassistant.components.syncthru pysyncthru==0.7.0 From d128443a2adfebbcbf2c398837e4c854b2de2c54 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 3 Sep 2020 18:32:57 +0200 Subject: [PATCH 608/862] Google assistant openclose (#39612) * Make sure we set discreteOnlyOpenClose for binary sensors * Mark switches that are assumed state as commandOnlyOnOff * Drop stray extra line * Fix pylint error Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- .../components/google_assistant/trait.py | 3 +++ .../google_assistant/test_smart_home.py | 5 ++++- tests/components/google_assistant/test_trait.py | 15 +++++++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 569b12e4730..5d2d59f2144 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -315,6 +315,8 @@ class OnOffTrait(_Trait): def sync_attributes(self): """Return OnOff attributes for a sync request.""" + if self.state.attributes.get(ATTR_ASSUMED_STATE, False): + return {"commandOnlyOnOff": True} return {} def query_attributes(self): @@ -1541,6 +1543,7 @@ class OpenCloseTrait(_Trait): response = {} if self.state.domain == binary_sensor.DOMAIN: response["queryOnlyOpenClose"] = True + response["discreteOnlyOpenClose"] = True return response def query_attributes(self): diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 1aadb39d5a8..78d403c2038 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -795,7 +795,10 @@ async def test_device_class_binary_sensor(hass, device_class, google_type): "agentUserId": "test-agent", "devices": [ { - "attributes": {"queryOnlyOpenClose": True}, + "attributes": { + "queryOnlyOpenClose": True, + "discreteOnlyOpenClose": True, + }, "id": "binary_sensor.demo_sensor", "name": {"name": "Demo Sensor"}, "traits": ["action.devices.traits.OpenClose"], diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index ae51ba76ffc..ac0db986f42 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -208,6 +208,11 @@ async def test_onoff_switch(hass): assert trt_off.query_attributes() == {"on": False} + trt_assumed = trait.OnOffTrait( + hass, State("switch.bla", STATE_OFF, {"assumed_state": True}), BASIC_CONFIG + ) + assert trt_assumed.sync_attributes() == {"commandOnlyOnOff": True} + on_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_ON) await trt_on.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": True}, {}) assert len(on_calls) == 1 @@ -2022,7 +2027,10 @@ async def test_openclose_binary_sensor(hass, device_class): BASIC_CONFIG, ) - assert trt.sync_attributes() == {"queryOnlyOpenClose": True} + assert trt.sync_attributes() == { + "queryOnlyOpenClose": True, + "discreteOnlyOpenClose": True, + } assert trt.query_attributes() == {"openPercent": 100} @@ -2032,7 +2040,10 @@ async def test_openclose_binary_sensor(hass, device_class): BASIC_CONFIG, ) - assert trt.sync_attributes() == {"queryOnlyOpenClose": True} + assert trt.sync_attributes() == { + "queryOnlyOpenClose": True, + "discreteOnlyOpenClose": True, + } assert trt.query_attributes() == {"openPercent": 0} From bde0bdbf8026a40f6fa6d50f4a82079a4fcba324 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 3 Sep 2020 18:39:24 +0200 Subject: [PATCH 609/862] Clean tradfri hass data and add tests (#39620) --- homeassistant/components/tradfri/__init__.py | 18 ++--- homeassistant/components/tradfri/cover.py | 7 +- homeassistant/components/tradfri/light.py | 6 +- homeassistant/components/tradfri/sensor.py | 7 +- homeassistant/components/tradfri/switch.py | 7 +- tests/components/tradfri/__init__.py | 1 + tests/components/tradfri/conftest.py | 71 +++++++++++++++++++- tests/components/tradfri/test_init.py | 53 ++++++++++++++- tests/components/tradfri/test_light.py | 47 +++---------- 9 files changed, 153 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index af63fe192cd..d3caaf54762 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,6 +1,5 @@ """Support for IKEA Tradfri.""" import asyncio -import logging from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory @@ -31,9 +30,8 @@ from .const import ( PLATFORMS, ) -_LOGGER = logging.getLogger(__name__) - FACTORY = "tradfri_factory" +LISTENERS = "tradfri_listeners" CONFIG_SCHEMA = vol.Schema( { @@ -98,6 +96,8 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Create a gateway.""" # host, identity, key, allow_tradfri_groups + tradfri_data = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} + listeners = tradfri_data[LISTENERS] = [] factory = await APIFactory.init( entry.data[CONF_HOST], @@ -109,7 +109,7 @@ async def async_setup_entry(hass, entry): """Close connection when hass stops.""" await factory.shutdown() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + listeners.append(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)) api = factory.request gateway = Gateway() @@ -120,9 +120,8 @@ async def async_setup_entry(hass, entry): await factory.shutdown() raise ConfigEntryNotReady from err - hass.data.setdefault(KEY_API, {})[entry.entry_id] = api - hass.data.setdefault(KEY_GATEWAY, {})[entry.entry_id] = gateway - tradfri_data = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} + tradfri_data[KEY_API] = api + tradfri_data[KEY_GATEWAY] = gateway tradfri_data[FACTORY] = factory dev_reg = await hass.helpers.device_registry.async_get_registry() @@ -156,10 +155,11 @@ async def async_unload_entry(hass, entry): ) ) if unload_ok: - hass.data[KEY_API].pop(entry.entry_id) - hass.data[KEY_GATEWAY].pop(entry.entry_id) tradfri_data = hass.data[DOMAIN].pop(entry.entry_id) factory = tradfri_data[FACTORY] await factory.shutdown() + # unsubscribe listeners + for listener in tradfri_data[LISTENERS]: + listener() return unload_ok diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 6d8669eea91..cab7b6bbab7 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -3,14 +3,15 @@ from homeassistant.components.cover import ATTR_POSITION, CoverEntity from .base_class import TradfriBaseDevice -from .const import ATTR_MODEL, CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY +from .const import ATTR_MODEL, CONF_GATEWAY_ID, DOMAIN, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): """Load Tradfri covers based on a config entry.""" gateway_id = config_entry.data[CONF_GATEWAY_ID] - api = hass.data[KEY_API][config_entry.entry_id] - gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] + tradfri_data = hass.data[DOMAIN][config_entry.entry_id] + api = tradfri_data[KEY_API] + gateway = tradfri_data[KEY_GATEWAY] devices_commands = await api(gateway.get_devices()) devices = await api(devices_commands) diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 4e44b452d33..29e096b2c49 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -21,6 +21,7 @@ from .const import ( ATTR_TRANSITION_TIME, CONF_GATEWAY_ID, CONF_IMPORT_GROUPS, + DOMAIN, KEY_API, KEY_GATEWAY, SUPPORTED_GROUP_FEATURES, @@ -33,8 +34,9 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Load Tradfri lights based on a config entry.""" gateway_id = config_entry.data[CONF_GATEWAY_ID] - api = hass.data[KEY_API][config_entry.entry_id] - gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] + tradfri_data = hass.data[DOMAIN][config_entry.entry_id] + api = tradfri_data[KEY_API] + gateway = tradfri_data[KEY_GATEWAY] devices_commands = await api(gateway.get_devices()) devices = await api(devices_commands) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index db12ab0a5cb..0cdd9152b4f 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -3,14 +3,15 @@ from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE from .base_class import TradfriBaseDevice -from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY +from .const import CONF_GATEWAY_ID, DOMAIN, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Tradfri config entry.""" gateway_id = config_entry.data[CONF_GATEWAY_ID] - api = hass.data[KEY_API][config_entry.entry_id] - gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] + tradfri_data = hass.data[DOMAIN][config_entry.entry_id] + api = tradfri_data[KEY_API] + gateway = tradfri_data[KEY_GATEWAY] devices_commands = await api(gateway.get_devices()) all_devices = await api(devices_commands) diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index cf23ffeb445..5bc5e6ab8e8 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -2,14 +2,15 @@ from homeassistant.components.switch import SwitchEntity from .base_class import TradfriBaseDevice -from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY +from .const import CONF_GATEWAY_ID, DOMAIN, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): """Load Tradfri switches based on a config entry.""" gateway_id = config_entry.data[CONF_GATEWAY_ID] - api = hass.data[KEY_API][config_entry.entry_id] - gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] + tradfri_data = hass.data[DOMAIN][config_entry.entry_id] + api = tradfri_data[KEY_API] + gateway = tradfri_data[KEY_GATEWAY] devices_commands = await api(gateway.get_devices()) devices = await api(devices_commands) diff --git a/tests/components/tradfri/__init__.py b/tests/components/tradfri/__init__.py index 4d1b505abc9..e7a6fcb9138 100644 --- a/tests/components/tradfri/__init__.py +++ b/tests/components/tradfri/__init__.py @@ -1 +1,2 @@ """Tests for the tradfri component.""" +MOCK_GATEWAY_ID = "mock-gateway-id" diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 891dd1377fe..a944f836dea 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -1,7 +1,11 @@ """Common tradfri test fixtures.""" import pytest -from tests.async_mock import patch +from . import MOCK_GATEWAY_ID + +from tests.async_mock import Mock, patch + +# pylint: disable=protected-access @pytest.fixture @@ -9,8 +13,8 @@ def mock_gateway_info(): """Mock get_gateway_info.""" with patch( "homeassistant.components.tradfri.config_flow.get_gateway_info" - ) as mock_gateway: - yield mock_gateway + ) as gateway_info: + yield gateway_info @pytest.fixture @@ -19,3 +23,64 @@ def mock_entry_setup(): with patch("homeassistant.components.tradfri.async_setup_entry") as mock_setup: mock_setup.return_value = True yield mock_setup + + +@pytest.fixture(name="gateway_id") +def mock_gateway_id_fixture(): + """Return mock gateway_id.""" + return MOCK_GATEWAY_ID + + +@pytest.fixture(name="mock_gateway") +def mock_gateway_fixture(gateway_id): + """Mock a Tradfri gateway.""" + + def get_devices(): + """Return mock devices.""" + return gateway.mock_devices + + def get_groups(): + """Return mock groups.""" + return gateway.mock_groups + + gateway_info = Mock(id=gateway_id, firmware_version="1.2.1234") + + def get_gateway_info(): + """Return mock gateway info.""" + return gateway_info + + gateway = Mock( + get_devices=get_devices, + get_groups=get_groups, + get_gateway_info=get_gateway_info, + mock_devices=[], + mock_groups=[], + mock_responses=[], + ) + with patch("homeassistant.components.tradfri.Gateway", return_value=gateway), patch( + "homeassistant.components.tradfri.config_flow.Gateway", return_value=gateway + ): + yield gateway + + +@pytest.fixture(name="mock_api") +def mock_api_fixture(mock_gateway): + """Mock api.""" + + async def api(command): + """Mock api function.""" + # Store the data for "real" command objects. + if hasattr(command, "_data") and not isinstance(command, Mock): + mock_gateway.mock_responses.append(command._data) + return command + + return api + + +@pytest.fixture(name="api_factory") +def mock_api_factory_fixture(mock_api): + """Mock pytradfri api factory.""" + with patch("homeassistant.components.tradfri.APIFactory", autospec=True) as factory: + factory.init.return_value = factory.return_value + factory.return_value.request = mock_api + yield factory.return_value diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 2845137244b..34cc6d38091 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -1,4 +1,9 @@ """Tests for Tradfri setup.""" +from homeassistant.components import tradfri +from homeassistant.helpers.device_registry import ( + async_entries_for_config_entry, + async_get_registry as async_get_device_registry, +) from homeassistant.setup import async_setup_component from tests.async_mock import patch @@ -48,13 +53,15 @@ async def test_config_json_host_not_imported(hass): assert len(mock_init.mock_calls) == 0 -async def test_config_json_host_imported(hass, mock_gateway_info, mock_entry_setup): +async def test_config_json_host_imported( + hass, mock_gateway_info, mock_entry_setup, gateway_id +): """Test that we import a configured host.""" mock_gateway_info.side_effect = lambda hass, host, identity, key: { "host": host, "identity": identity, "key": key, - "gateway_id": "mock-gateway", + "gateway_id": gateway_id, } with patch( @@ -68,3 +75,45 @@ async def test_config_json_host_imported(hass, mock_gateway_info, mock_entry_set assert config_entry.domain == "tradfri" assert config_entry.source == "import" assert config_entry.title == "mock-host" + + +async def test_entry_setup_unload(hass, api_factory, gateway_id): + """Test config entry setup and unload.""" + entry = MockConfigEntry( + domain=tradfri.DOMAIN, + data={ + tradfri.CONF_HOST: "mock-host", + tradfri.CONF_IDENTITY: "mock-identity", + tradfri.CONF_KEY: "mock-key", + tradfri.CONF_IMPORT_GROUPS: True, + tradfri.CONF_GATEWAY_ID: gateway_id, + }, + ) + + entry.add_to_hass(hass) + with patch.object( + hass.config_entries, "async_forward_entry_setup", return_value=True + ) as setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert setup.call_count == len(tradfri.PLATFORMS) + + dev_reg = await async_get_device_registry(hass) + dev_entries = async_entries_for_config_entry(dev_reg, entry.entry_id) + + assert dev_entries + dev_entry = dev_entries[0] + assert dev_entry.identifiers == { + (tradfri.DOMAIN, entry.data[tradfri.CONF_GATEWAY_ID]) + } + assert dev_entry.manufacturer == tradfri.ATTR_TRADFRI_MANUFACTURER + assert dev_entry.name == tradfri.ATTR_TRADFRI_GATEWAY + assert dev_entry.model == tradfri.ATTR_TRADFRI_GATEWAY_MODEL + + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=True + ) as unload: + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert unload.call_count == len(tradfri.PLATFORMS) + assert api_factory.shutdown.call_count == 1 diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index a5a5823fbf4..b4c209c1493 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -9,6 +9,8 @@ from pytradfri.device.light_control import LightControl from homeassistant.components import tradfri +from . import MOCK_GATEWAY_ID + from tests.async_mock import MagicMock, Mock, PropertyMock, patch from tests.common import MockConfigEntry @@ -93,42 +95,6 @@ def setup(request): request.addfinalizer(teardown) -@pytest.fixture -def mock_gateway(): - """Mock a Tradfri gateway.""" - - def get_devices(): - """Return mock devices.""" - return gateway.mock_devices - - def get_groups(): - """Return mock groups.""" - return gateway.mock_groups - - gateway = Mock( - get_devices=get_devices, - get_groups=get_groups, - mock_devices=[], - mock_groups=[], - mock_responses=[], - ) - return gateway - - -@pytest.fixture -def mock_api(mock_gateway): - """Mock api.""" - - async def api(command): - """Mock api function.""" - # Store the data for "real" command objects. - if hasattr(command, "_data") and not isinstance(command, Mock): - mock_gateway.mock_responses.append(command._data) - return command - - return api - - async def generate_psk(self, code): """Mock psk.""" return "mock" @@ -143,11 +109,14 @@ async def setup_gateway(hass, mock_gateway, mock_api): "identity": "mock-identity", "key": "mock-key", "import_groups": True, - "gateway_id": "mock-gateway-id", + "gateway_id": MOCK_GATEWAY_ID, }, ) - hass.data[tradfri.KEY_GATEWAY] = {entry.entry_id: mock_gateway} - hass.data[tradfri.KEY_API] = {entry.entry_id: mock_api} + tradfri_data = {} + hass.data[tradfri.DOMAIN] = {entry.entry_id: tradfri_data} + tradfri_data[tradfri.KEY_API] = mock_api + tradfri_data[tradfri.KEY_GATEWAY] = mock_gateway + await hass.config_entries.async_forward_entry_setup(entry, "light") await hass.async_block_till_done() From a0663d84d2176497fbcf86dd0393928fcffeb62a Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 3 Sep 2020 11:54:04 -0500 Subject: [PATCH 610/862] fix black for stream (#39622) --- homeassistant/components/stream/worker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index b461d17cf1c..d0ba30666b0 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -50,7 +50,9 @@ def stream_worker(hass, stream, quit_event): # To avoid excessive restarts, don't restart faster than once every 40 seconds. wait_timeout = max(40 - (time.time() - start_time), 0) _LOGGER.debug( - "Restarting stream worker in %d seconds: %s", wait_timeout, stream.source, + "Restarting stream worker in %d seconds: %s", + wait_timeout, + stream.source, ) From 17b4c1a2ca892ce268a90e12ce4a41b56b0c375b Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 3 Sep 2020 12:11:14 -0500 Subject: [PATCH 611/862] Use CoordinatorEntity for supla (#39621) --- homeassistant/components/supla/__init__.py | 28 +++++----------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 8912ee8a59b..40313a41553 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -11,8 +11,10 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) _LOGGER = logging.getLogger(__name__) @@ -154,26 +156,21 @@ async def discover_devices(hass, hass_config): await async_load_platform(hass, component_name, DOMAIN, config, hass_config) -class SuplaChannel(Entity): +class SuplaChannel(CoordinatorEntity): """Base class of a Supla Channel (an equivalent of HA's Entity).""" def __init__(self, config, server, coordinator): """Init from config, hookup[ server and coordinator.""" + super().__init__(coordinator) self.server_name = config["server_name"] self.channel_id = config["channel_id"] self.server = server - self.coordinator = coordinator @property def channel_data(self): """Return channel data taken from coordinator.""" return self.coordinator.data.get(self.channel_id) - @property - def should_poll(self): - """Supla uses DataUpdateCoordinator, so no additional polling needed.""" - return False - @property def unique_id(self) -> str: """Return a unique ID.""" @@ -197,12 +194,6 @@ class SuplaChannel(Entity): return False return state.get("connected") - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - async def async_action(self, action, **add_pars): """ Run server action. @@ -220,10 +211,3 @@ class SuplaChannel(Entity): # Update state await self.coordinator.async_request_refresh() - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self.coordinator.async_request_refresh() From a9cc88239499a1fd1de3f92c1e4d0ed9cf7c77d3 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 3 Sep 2020 12:23:02 -0500 Subject: [PATCH 612/862] Improve mpd media position handling (#39390) * improve mpd media position handling * Update media_player.py * Update media_player.py * Update media_player.py --- homeassistant/components/mpd/media_player.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 201e8ed64e1..8a46cef6eb3 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -133,10 +133,17 @@ class MpdDevice(MediaPlayerEntity): self._status = self._client.status() self._currentsong = self._client.currentsong() - position = self._status.get("time") + position = self._status.get("elapsed") + + if position is None: + position = self._status.get("time") + + if position is not None and ":" in position: + position = position.split(":")[0] + if position is not None and self._media_position != position: self._media_position_updated_at = dt_util.utcnow() - self._media_position = position + self._media_position = int(position) self._update_playlists() From 35e84d04276f72c6242f1fd09388679506b770ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Sep 2020 14:35:16 -0500 Subject: [PATCH 613/862] Add as_local convenience function to jinja templates (#39618) --- homeassistant/helpers/template.py | 2 ++ tests/helpers/test_template.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index c4c8f3a02f0..eeb43fb8756 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1051,6 +1051,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["atan2"] = arc_tangent2 self.filters["sqrt"] = square_root self.filters["as_timestamp"] = forgiving_as_timestamp + self.filters["as_local"] = dt_util.as_local self.filters["timestamp_custom"] = timestamp_custom self.filters["timestamp_local"] = timestamp_local self.filters["timestamp_utc"] = timestamp_utc @@ -1084,6 +1085,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["atan2"] = arc_tangent2 self.globals["float"] = forgiving_float self.globals["now"] = dt_util.now + self.globals["as_local"] = dt_util.as_local self.globals["utcnow"] = dt_util.utcnow self.globals["as_timestamp"] = forgiving_as_timestamp self.globals["relative_time"] = relative_time diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 5b32634d8f0..237e529652a 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -511,6 +511,19 @@ def test_timestamp_local(hass): ) +def test_as_local(hass): + """Test converting time to local.""" + + hass.states.async_set("test.object", "available") + last_updated = hass.states.get("test.object").last_updated + assert template.Template( + "{{ as_local(states.test.object.last_updated) }}", hass + ).async_render() == str(dt_util.as_local(last_updated)) + assert template.Template( + "{{ states.test.object.last_updated | as_local }}", hass + ).async_render() == str(dt_util.as_local(last_updated)) + + def test_to_json(hass): """Test the object to JSON string filter.""" From c1b8497aaaded97667a27df61f47dd9da5f44d0e Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 3 Sep 2020 15:32:48 -0500 Subject: [PATCH 614/862] Improve sonarr sensor test (#39623) * improve sonarr sensor test * Update test_sensor.py * Update test_sensor.py * Update test_sensor.py * Update test_sensor.py * Update test_sensor.py * Update test_sensor.py --- tests/components/sonarr/test_sensor.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 8fbca025404..93c8d7d4d9e 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -4,11 +4,8 @@ from datetime import timedelta import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.sonarr.const import ( - CONF_BASE_PATH, - CONF_UPCOMING_DAYS, - DOMAIN, -) +from homeassistant.components.sonarr.const import CONF_BASE_PATH, DOMAIN +from homeassistant.config_entries import ENTRY_STATE_LOADED from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, @@ -23,6 +20,8 @@ from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.sonarr import ( MOCK_SENSOR_CONFIG, + _patch_async_setup, + _patch_async_setup_entry, mock_connection, setup_integration, ) @@ -36,18 +35,18 @@ async def test_import_from_sensor_component( ) -> None: """Test import from sensor platform.""" mock_connection(aioclient_mock) - assert await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MOCK_SENSOR_CONFIG} - ) - await hass.async_block_till_done() + + with _patch_async_setup(), _patch_async_setup_entry(): + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MOCK_SENSOR_CONFIG} + ) + await hass.async_block_till_done() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_LOADED assert entries[0].data[CONF_BASE_PATH] == "/api" - assert entries[0].options[CONF_UPCOMING_DAYS] == 3 - - assert hass.states.get(UPCOMING_ENTITY_ID) async def test_sensors( From a87fedc0af3e2a9bcec745f924fb4534c3b57cea Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 3 Sep 2020 23:41:24 +0300 Subject: [PATCH 615/862] Automatically configure HTTP auth type in ONVIF snapshots (#38729) * Allow selection of HTTP auth type in ONVIF snapshots * Auto populate snapshot auth * Fix no auth case * Add missing return --- homeassistant/components/onvif/__init__.py | 42 +++++++++++++++++++ homeassistant/components/onvif/camera.py | 13 +++++- homeassistant/components/onvif/const.py | 1 + .../components/onvif/translations/en.json | 8 ++-- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index bb8008e1fef..93e948b770b 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1,6 +1,9 @@ """The ONVIF integration.""" import asyncio +import requests +from requests.auth import HTTPDigestAuth +from urllib3.exceptions import ReadTimeoutError import voluptuous as vol from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS @@ -12,6 +15,8 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -19,6 +24,7 @@ from homeassistant.helpers import config_per_platform from .const import ( CONF_RTSP_TRANSPORT, + CONF_SNAPSHOT_AUTH, DEFAULT_ARGUMENTS, DEFAULT_NAME, DEFAULT_PASSWORD, @@ -76,6 +82,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not device.available: raise ConfigEntryNotReady() + if not entry.data.get(CONF_SNAPSHOT_AUTH): + await async_populate_snapshot_auth(hass, device, entry) + hass.data[DOMAIN][entry.unique_id] = device platforms = ["camera"] @@ -113,6 +122,39 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) +async def _get_snapshot_auth(hass, device, entry): + if not (device.username and device.password): + return HTTP_DIGEST_AUTHENTICATION + + snapshot_uri = await device.async_get_snapshot_uri(device.profiles[0]) + auth = HTTPDigestAuth(device.username, device.password) + + def _get(): + # so we can handle keyword arguments + return requests.get(snapshot_uri, timeout=1, auth=auth) + + try: + response = await hass.async_add_executor_job(_get) + + if response.status_code == 401: + return HTTP_BASIC_AUTHENTICATION + + return HTTP_DIGEST_AUTHENTICATION + except requests.exceptions.Timeout: + return HTTP_BASIC_AUTHENTICATION + except requests.exceptions.ConnectionError as error: + if isinstance(error.args[0], ReadTimeoutError): + return HTTP_BASIC_AUTHENTICATION + return HTTP_DIGEST_AUTHENTICATION + + +async def async_populate_snapshot_auth(hass, device, entry): + """Check if digest auth for snapshots is possible.""" + auth = await _get_snapshot_auth(hass, device, entry) + new_data = {**entry.data, CONF_SNAPSHOT_AUTH: auth} + hass.config_entries.async_update_entry(entry, data=new_data) + + async def async_populate_options(hass, entry): """Populate default options for device.""" options = { diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 7f97e0fbea4..5c2f89adf32 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -4,11 +4,12 @@ import asyncio from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame import requests -from requests.auth import HTTPDigestAuth +from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG +from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream @@ -24,6 +25,7 @@ from .const import ( ATTR_TILT, ATTR_ZOOM, CONF_RTSP_TRANSPORT, + CONF_SNAPSHOT_AUTH, CONTINUOUS_MOVE, DIR_DOWN, DIR_LEFT, @@ -79,6 +81,10 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): self.stream_options[CONF_RTSP_TRANSPORT] = device.config_entry.options.get( CONF_RTSP_TRANSPORT ) + self._basic_auth = ( + device.config_entry.data.get(CONF_SNAPSHOT_AUTH) + == HTTP_BASIC_AUTHENTICATION + ) self._stream_uri = None self._snapshot_uri = None @@ -115,7 +121,10 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): if self.device.capabilities.snapshot: auth = None if self.device.username and self.device.password: - auth = HTTPDigestAuth(self.device.username, self.device.password) + if self._basic_auth: + auth = HTTPBasicAuth(self.device.username, self.device.password) + else: + auth = HTTPDigestAuth(self.device.username, self.device.password) def fetch(): """Read image from a URL.""" diff --git a/homeassistant/components/onvif/const.py b/homeassistant/components/onvif/const.py index ddc1cc22801..2ac78622f05 100644 --- a/homeassistant/components/onvif/const.py +++ b/homeassistant/components/onvif/const.py @@ -13,6 +13,7 @@ DEFAULT_ARGUMENTS = "-pred 1" CONF_DEVICE_ID = "deviceid" CONF_RTSP_TRANSPORT = "rtsp_transport" +CONF_SNAPSHOT_AUTH = "snapshot_auth" RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"] diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json index a20b1fcb7e4..df47900fbc5 100644 --- a/homeassistant/components/onvif/translations/en.json +++ b/homeassistant/components/onvif/translations/en.json @@ -13,8 +13,8 @@ "step": { "auth": { "data": { - "password": "Password", - "username": "Username" + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" }, "title": "Configure authentication" }, @@ -33,9 +33,9 @@ }, "manual_input": { "data": { - "host": "Host", + "host": "[%key:common::config_flow::data::host%]", "name": "Name", - "port": "Port" + "port": "[%key:common::config_flow::data::port%]" }, "title": "Configure ONVIF device" }, From 77f5fb765b1ad7f2f886f65851be82f0437fd3b2 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 3 Sep 2020 16:06:24 -0500 Subject: [PATCH 616/862] Add device class for roku devices (#39627) * add tv device class for roku tvs * Update test_media_player.py * Update test_media_player.py * Update media_player.py * Update test_media_player.py * Update media_player.py * Update test_media_player.py * Update test_media_player.py * Update media_player.py --- homeassistant/components/roku/media_player.py | 16 ++++++++++++++-- tests/components/roku/test_media_player.py | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 96d23872dfa..1aa4fc3e9bb 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -1,10 +1,14 @@ """Support for the Roku media player.""" import logging -from typing import List +from typing import List, Optional import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import ( + DEVICE_CLASS_RECEIVER, + DEVICE_CLASS_TV, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, @@ -90,6 +94,14 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Return the unique ID for this entity.""" return self._unique_id + @property + def device_class(self) -> Optional[str]: + """Return the class of this device.""" + if self.coordinator.data.info.device_type == "tv": + return DEVICE_CLASS_TV + + return DEVICE_CLASS_RECEIVER + @property def state(self) -> str: """Return the state of the device.""" diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 4df0c95ad07..b355e6178ff 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -3,6 +3,7 @@ from datetime import timedelta from rokuecp import RokuError +from homeassistant.components.media_player import DEVICE_CLASS_RECEIVER, DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( ATTR_APP_ID, ATTR_APP_NAME, @@ -77,6 +78,7 @@ async def test_setup( assert hass.states.get(MAIN_ENTITY_ID) assert main + assert main.device_class == DEVICE_CLASS_RECEIVER assert main.unique_id == UPNP_SERIAL @@ -108,6 +110,7 @@ async def test_tv_setup( assert hass.states.get(TV_ENTITY_ID) assert tv + assert tv.device_class == DEVICE_CLASS_TV assert tv.unique_id == TV_SERIAL From d0120d5e0ad02f4be55af1bbcbe6b5aece50c964 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Thu, 3 Sep 2020 23:19:45 +0200 Subject: [PATCH 617/862] Update DSMR integration to import yaml to ConfigEntry (#39473) * Rewrite to import from platform setup * Add config flow for import * Implement reload * Update sensor tests * Add config flow tests * Remove some code * Fix pylint issue * Remove update options code * Add platform import test * Remove infinite while loop * Move async_setup_platform * Check for unload_ok * Remove commented out test code * Implement function to check on host/port already existing Co-authored-by: Chris Talkington * Implement new method in import * Update tests * Fix test setup platform * Add string * Patch setup_platform * Add block till done to patch block Co-authored-by: Chris Talkington --- CODEOWNERS | 1 + homeassistant/components/dsmr/__init__.py | 53 +++++ homeassistant/components/dsmr/config_flow.py | 61 ++++++ homeassistant/components/dsmr/const.py | 21 ++ homeassistant/components/dsmr/manifest.json | 3 +- homeassistant/components/dsmr/sensor.py | 101 +++++---- homeassistant/components/dsmr/strings.json | 9 + tests/components/dsmr/test_config_flow.py | 102 +++++++++ tests/components/dsmr/test_sensor.py | 215 ++++++++++++++++--- 9 files changed, 498 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/dsmr/config_flow.py create mode 100644 homeassistant/components/dsmr/const.py create mode 100644 homeassistant/components/dsmr/strings.json create mode 100644 tests/components/dsmr/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 0f0757cb983..ddd36cc7da8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -102,6 +102,7 @@ homeassistant/components/digital_ocean/* @fabaff homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 @bdraco +homeassistant/components/dsmr/* @Robbie1221 homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dunehd/* @bieniu homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95 diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 107e221a465..7eaa57fe8d3 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -1 +1,54 @@ """The dsmr component.""" +import asyncio +from asyncio import CancelledError +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DATA_TASK, DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config: dict): + """Set up the DSMR platform.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up DSMR from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + task = hass.data[DOMAIN][entry.entry_id][DATA_TASK] + + # Cancel the reconnect task + task.cancel() + try: + await task + except CancelledError: + pass + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py new file mode 100644 index 00000000000..ecf93177334 --- /dev/null +++ b/homeassistant/components/dsmr/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow for DSMR integration.""" +import logging +from typing import Any, Dict, Optional + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for DSMR.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def _abort_if_host_port_configured( + self, + port: str, + host: str = None, + updates: Optional[Dict[Any, Any]] = None, + reload_on_update: bool = True, + ): + """Test if host and port are already configured.""" + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data.get(CONF_HOST) == host and entry.data[CONF_PORT] == port: + if updates is not None: + changed = self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **updates} + ) + if ( + changed + and reload_on_update + and entry.state + in ( + config_entries.ENTRY_STATE_LOADED, + config_entries.ENTRY_STATE_SETUP_RETRY, + ) + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + + async def async_step_import(self, import_config=None): + """Handle the initial step.""" + host = import_config.get(CONF_HOST) + port = import_config[CONF_PORT] + + status = self._abort_if_host_port_configured(port, host, import_config) + if status is not None: + return status + + if host is not None: + name = f"{host}:{port}" + else: + name = port + + return self.async_create_entry(title=name, data=import_config) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py new file mode 100644 index 00000000000..110e6b46a99 --- /dev/null +++ b/homeassistant/components/dsmr/const.py @@ -0,0 +1,21 @@ +"""Constants for the DSMR integration.""" + +DOMAIN = "dsmr" + +PLATFORMS = ["sensor"] + +CONF_DSMR_VERSION = "dsmr_version" +CONF_RECONNECT_INTERVAL = "reconnect_interval" +CONF_PRECISION = "precision" + +DEFAULT_DSMR_VERSION = "2.2" +DEFAULT_PORT = "/dev/ttyUSB0" +DEFAULT_PRECISION = 3 +DEFAULT_RECONNECT_INTERVAL = 30 + +DATA_TASK = "task" + +ICON_GAS = "mdi:fire" +ICON_POWER = "mdi:flash" +ICON_POWER_FAILURE = "mdi:flash-off" +ICON_SWELL_SAG = "mdi:pulse" diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 42e6b81dc1f..964c68ab182 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -3,5 +3,6 @@ "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", "requirements": ["dsmr_parser==0.18"], - "codeowners": [] + "codeowners": ["@Robbie1221"], + "config_flow": false } diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 4d780d48cd1..4298afe3cf6 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -1,5 +1,6 @@ """Support for Dutch Smart Meter (also known as Smartmeter or P1 port).""" import asyncio +from asyncio import CancelledError from functools import partial import logging @@ -9,6 +10,7 @@ import serial import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -18,26 +20,26 @@ from homeassistant.const import ( from homeassistant.core import CoreState, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + CONF_DSMR_VERSION, + CONF_PRECISION, + CONF_RECONNECT_INTERVAL, + DATA_TASK, + DEFAULT_DSMR_VERSION, + DEFAULT_PORT, + DEFAULT_PRECISION, + DEFAULT_RECONNECT_INTERVAL, + DOMAIN, + ICON_GAS, + ICON_POWER, + ICON_POWER_FAILURE, + ICON_SWELL_SAG, +) _LOGGER = logging.getLogger(__name__) -CONF_DSMR_VERSION = "dsmr_version" -CONF_RECONNECT_INTERVAL = "reconnect_interval" -CONF_PRECISION = "precision" - -DEFAULT_DSMR_VERSION = "2.2" -DEFAULT_PORT = "/dev/ttyUSB0" -DEFAULT_PRECISION = 3 - -DOMAIN = "dsmr" - -ICON_GAS = "mdi:fire" -ICON_POWER = "mdi:flash" -ICON_POWER_FAILURE = "mdi:flash-off" -ICON_SWELL_SAG = "mdi:pulse" - -RECONNECT_INTERVAL = 5 - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, @@ -45,17 +47,30 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( cv.string, vol.In(["5B", "5", "4", "2.2"]) ), - vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int, + vol.Optional(CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL): int, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), } ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import the platform into a config entry.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up the DSMR sensor.""" # Suppress logging logging.getLogger("dsmr_parser").setLevel(logging.ERROR) + config = entry.data + dsmr_version = config[CONF_DSMR_VERSION] # Define list of name,obis mappings to generate entities @@ -141,31 +156,24 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Start DSMR asyncio.Protocol reader try: transport, protocol = await hass.loop.create_task(reader_factory()) - except ( - serial.serialutil.SerialException, - ConnectionRefusedError, - TimeoutError, - ): - # Log any error while establishing connection and drop to retry - # connection wait - _LOGGER.exception("Error connecting to DSMR") - transport = None - if transport: - # Register listener to close transport on HA shutdown - stop_listener = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, transport.close - ) + if transport: + # Register listener to close transport on HA shutdown + stop_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, transport.close + ) - # Wait for reader to close - await protocol.wait_closed() + # Wait for reader to close + await protocol.wait_closed() - if hass.state != CoreState.stopping: # Unexpected disconnect if transport: # remove listener stop_listener() + transport = None + protocol = None + # Reflect disconnect state in devices state by setting an # empty telegram resulting in `unknown` states update_entities_telegram({}) @@ -173,8 +181,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # throttle reconnect attempts await asyncio.sleep(config[CONF_RECONNECT_INTERVAL]) + except (serial.serialutil.SerialException, OSError): + # Log any error while establishing connection and drop to retry + # connection wait + _LOGGER.exception("Error connecting to DSMR") + transport = None + protocol = None + except CancelledError: + if stop_listener: + stop_listener() + + if transport: + transport.close() + + if protocol: + await protocol.wait_closed() + + return + # Can't be hass.async_add_job because job runs forever - hass.loop.create_task(connect_and_reconnect()) + task = hass.loop.create_task(connect_and_reconnect()) + + # Save the task to be able to cancel it when unloading + hass.data[DOMAIN][entry.entry_id][DATA_TASK] = task class DSMREntity(Entity): diff --git a/homeassistant/components/dsmr/strings.json b/homeassistant/components/dsmr/strings.json new file mode 100644 index 00000000000..bc498971960 --- /dev/null +++ b/homeassistant/components/dsmr/strings.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": {}, + "error": {}, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py new file mode 100644 index 00000000000..1d25d2cd915 --- /dev/null +++ b/tests/components/dsmr/test_config_flow.py @@ -0,0 +1,102 @@ +"""Test the DSMR config flow.""" +from homeassistant import config_entries, setup +from homeassistant.components.dsmr import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_import_usb(hass): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "/dev/ttyUSB0" + assert result["data"] == entry_data + + +async def test_import_network(hass): + """Test we can import from network.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "host": "localhost", + "port": "1234", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "localhost:1234" + assert result["data"] == entry_data + + +async def test_import_update(hass): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + entry = MockConfigEntry( + domain=DOMAIN, + data=entry_data, + unique_id="/dev/ttyUSB0", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dsmr.async_setup_entry", return_value=True + ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + new_entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 3, + "reconnect_interval": 30, + } + + with patch( + "homeassistant.components.dsmr.async_setup_entry", return_value=True + ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=new_entry_data, + ) + + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + assert entry.data["precision"] == 3 diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 95608aedba7..73c11579070 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -12,13 +12,15 @@ from itertools import chain, repeat import pytest -from homeassistant.bootstrap import async_setup_component +from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.dsmr.sensor import DerivativeDSMREntity +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ENERGY_KILO_WATT_HOUR, TIME_HOURS, VOLUME_CUBIC_METERS +from homeassistant.setup import async_setup_component import tests.async_mock -from tests.async_mock import DEFAULT, Mock -from tests.common import assert_setup_component +from tests.async_mock import DEFAULT, MagicMock, Mock +from tests.common import MockConfigEntry, patch @pytest.fixture @@ -47,6 +49,39 @@ def mock_connection_factory(monkeypatch): return connection_factory, transport, protocol +async def test_setup_platform(hass, mock_connection_factory): + """Test setup of platform.""" + async_add_entities = MagicMock() + + entry_data = { + "platform": DOMAIN, + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + with patch("homeassistant.components.dsmr.async_setup", return_value=True), patch( + "homeassistant.components.dsmr.async_setup_entry", return_value=True + ): + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: entry_data} + ) + await hass.async_block_till_done() + + assert not async_add_entities.called + + # Check config entry + conf_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(conf_entries) == 1 + + entry = conf_entries[0] + + assert entry.state == "loaded" + assert entry.data == entry_data + + async def test_default_setup(hass, mock_connection_factory): """Test the default setup.""" (connection_factory, transport, protocol) = mock_connection_factory @@ -58,7 +93,12 @@ async def test_default_setup(hass, mock_connection_factory): ) from dsmr_parser.objects import CosemObject, MBusObject - config = {"platform": "dsmr"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } telegram = { CURRENT_ELECTRICITY_USAGE: CosemObject( @@ -73,9 +113,14 @@ async def test_default_setup(hass, mock_connection_factory): ), } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -107,6 +152,10 @@ async def test_default_setup(hass, mock_connection_factory): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_derivative(): """Test calculation of derivative value.""" @@ -158,7 +207,12 @@ async def test_v4_meter(hass, mock_connection_factory): ) from dsmr_parser.objects import CosemObject, MBusObject - config = {"platform": "dsmr", "dsmr_version": "4"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "4", + "precision": 4, + "reconnect_interval": 30, + } telegram = { HOURLY_GAS_METER_READING: MBusObject( @@ -170,9 +224,14 @@ async def test_v4_meter(hass, mock_connection_factory): ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -192,6 +251,10 @@ async def test_v4_meter(hass, mock_connection_factory): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_v5_meter(hass, mock_connection_factory): """Test if v5 meter is correctly parsed.""" @@ -203,7 +266,12 @@ async def test_v5_meter(hass, mock_connection_factory): ) from dsmr_parser.objects import CosemObject, MBusObject - config = {"platform": "dsmr", "dsmr_version": "5"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5", + "precision": 4, + "reconnect_interval": 30, + } telegram = { HOURLY_GAS_METER_READING: MBusObject( @@ -215,9 +283,14 @@ async def test_v5_meter(hass, mock_connection_factory): ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -237,6 +310,10 @@ async def test_v5_meter(hass, mock_connection_factory): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_belgian_meter(hass, mock_connection_factory): """Test if Belgian meter is correctly parsed.""" @@ -248,7 +325,12 @@ async def test_belgian_meter(hass, mock_connection_factory): ) from dsmr_parser.objects import CosemObject, MBusObject - config = {"platform": "dsmr", "dsmr_version": "5B"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "precision": 4, + "reconnect_interval": 30, + } telegram = { BELGIUM_HOURLY_GAS_METER_READING: MBusObject( @@ -260,9 +342,14 @@ async def test_belgian_meter(hass, mock_connection_factory): ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]), } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -282,6 +369,10 @@ async def test_belgian_meter(hass, mock_connection_factory): assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_belgian_meter_low(hass, mock_connection_factory): """Test if Belgian meter is correctly parsed.""" @@ -290,13 +381,23 @@ async def test_belgian_meter_low(hass, mock_connection_factory): from dsmr_parser.obis_references import ELECTRICITY_ACTIVE_TARIFF from dsmr_parser.objects import CosemObject - config = {"platform": "dsmr", "dsmr_version": "5B"} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5B", + "precision": 4, + "reconnect_interval": 30, + } telegram = {ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0002", "unit": ""}])} - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -311,26 +412,50 @@ async def test_belgian_meter_low(hass, mock_connection_factory): assert power_tariff.state == "low" assert power_tariff.attributes.get("unit_of_measurement") == "" + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_tcp(hass, mock_connection_factory): """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = mock_connection_factory - config = {"platform": "dsmr", "host": "localhost", "port": 1234} + entry_data = { + "host": "localhost", + "port": "1234", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } - with assert_setup_component(1): - await async_setup_component(hass, "sensor", {"sensor": config}) - await hass.async_block_till_done() + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() assert connection_factory.call_args_list[0][0][0] == "localhost" assert connection_factory.call_args_list[0][0][1] == "1234" + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): """Connection should be retried on error during setup.""" (connection_factory, transport, protocol) = mock_connection_factory - config = {"platform": "dsmr", "reconnect_interval": 0} + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 0, + } # override the mock to have it fail the first time and succeed after first_fail_connection_factory = tests.async_mock.AsyncMock( @@ -342,17 +467,35 @@ async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factor "homeassistant.components.dsmr.sensor.create_dsmr_reader", first_fail_connection_factory, ) - await async_setup_component(hass, "sensor", {"sensor": config}) + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() # wait for sleep to resolve await hass.async_block_till_done() assert first_fail_connection_factory.call_count >= 2, "connecting not retried" + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" + async def test_reconnect(hass, monkeypatch, mock_connection_factory): """If transport disconnects, the connection should be retried.""" (connection_factory, transport, protocol) = mock_connection_factory - config = {"platform": "dsmr", "reconnect_interval": 0} + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 0, + } # mock waiting coroutine while connection lasts closed = asyncio.Event() @@ -365,7 +508,13 @@ async def test_reconnect(hass, monkeypatch, mock_connection_factory): protocol.wait_closed = wait_closed - await async_setup_component(hass, "sensor", {"sensor": config}) + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert connection_factory.call_count == 1 @@ -382,3 +531,7 @@ async def test_reconnect(hass, monkeypatch, mock_connection_factory): assert connection_factory.call_count >= 2, "connecting not retried" # setting it so teardown can be successful closed.set() + + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == "not_loaded" From f6a3eea7f21a67ab269afd30c88e7c7a99efb417 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 3 Sep 2020 16:27:13 -0500 Subject: [PATCH 618/862] Add device class to directv devices (#39628) * add device class to directv devices * Update test_media_player.py * Update media_player.py * Update test_media_player.py * Update media_player.py --- homeassistant/components/directv/media_player.py | 12 ++++++++++-- tests/components/directv/test_media_player.py | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index acf30cb103a..127fb5c04b2 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -1,10 +1,13 @@ """Support for the DirecTV receivers.""" import logging -from typing import Callable, List +from typing import Callable, List, Optional from directv import DIRECTV -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import ( + DEVICE_CLASS_RECEIVER, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -137,6 +140,11 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): """Return the name of the device.""" return self._name + @property + def device_class(self) -> Optional[str]: + """Return the class of this device.""" + return DEVICE_CLASS_RECEIVER + @property def unique_id(self): """Return a unique ID to use for this media player.""" diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 7203325fbbe..11aaad707b7 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -10,6 +10,7 @@ from homeassistant.components.directv.media_player import ( ATTR_MEDIA_RECORDED, ATTR_MEDIA_START_TIME, ) +from homeassistant.components.media_player import DEVICE_CLASS_RECEIVER from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ALBUM_NAME, @@ -169,12 +170,15 @@ async def test_unique_id( entity_registry = await hass.helpers.entity_registry.async_get_registry() main = entity_registry.async_get(MAIN_ENTITY_ID) + assert main.device_class == DEVICE_CLASS_RECEIVER assert main.unique_id == "028877455858" client = entity_registry.async_get(CLIENT_ENTITY_ID) + assert client.device_class == DEVICE_CLASS_RECEIVER assert client.unique_id == "2CA17D1CD30X" unavailable_client = entity_registry.async_get(UNAVAILABLE_ENTITY_ID) + assert unavailable_client.device_class == DEVICE_CLASS_RECEIVER assert unavailable_client.unique_id == "9XXXXXXXXXX9" From da82d171e0c13d59dbd2a1ab37709fc5428a49dc Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 3 Sep 2020 23:47:32 +0200 Subject: [PATCH 619/862] Add radio channel attribute to Sonos (#39631) --- homeassistant/components/sonos/manifest.json | 2 +- homeassistant/components/sonos/media_player.py | 15 +++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 3a8ba58cc61..efad23ee1f2 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.32"], + "requirements": ["pysonos==0.0.33"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 11c42e072fd..3ad22de9151 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -482,6 +482,7 @@ class SonosEntity(MediaPlayerEntity): self._media_position = None self._media_position_updated_at = None self._media_image_url = None + self._media_channel = None self._media_artist = None self._media_album_name = None self._media_title = None @@ -692,6 +693,7 @@ class SonosEntity(MediaPlayerEntity): self._uri = None self._media_duration = None self._media_image_url = None + self._media_channel = None self._media_artist = None self._media_album_name = None self._media_title = None @@ -765,10 +767,13 @@ class SonosEntity(MediaPlayerEntity): except (TypeError, KeyError, AttributeError): pass + media_info = self.soco.get_current_media_info() + + self._media_channel = media_info["channel"] + # Check if currently playing radio station is in favorites - media_info = self.soco.avTransport.GetMediaInfo([("InstanceID", 0)]) for fav in self._favorites: - if fav.reference.get_uri() == media_info["CurrentURI"]: + if fav.reference.get_uri() == media_info["uri"]: self._source_name = fav.title def update_media_music(self, update_media_position, track_info): @@ -955,6 +960,12 @@ class SonosEntity(MediaPlayerEntity): """Image url of current playing media.""" return self._media_image_url or None + @property + @soco_coordinator + def media_channel(self): + """Channel currently playing.""" + return self._media_channel or None + @property @soco_coordinator def media_artist(self): diff --git a/requirements_all.txt b/requirements_all.txt index ff53d0e6dd6..534a236ac33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1649,7 +1649,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.32 +pysonos==0.0.33 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ec4d5a09db..206e6448bef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -791,7 +791,7 @@ pysmartthings==0.7.3 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.32 +pysonos==0.0.33 # homeassistant.components.spc pyspcwebgw==0.4.0 From 353b40b28aabe3ff44aaef73bba18d591a57b740 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Thu, 3 Sep 2020 18:51:08 -0300 Subject: [PATCH 620/862] Add tests for Broadlink remotes (#39235) * Add tests for Broadlink remotes * Reformat the tests with Black * Add a helper method for device setup * Rename device.setup() to device.setup_entry() * Apply suggestions from code review Co-authored-by: Chris Talkington --- tests/components/broadlink/__init__.py | 17 ++- tests/components/broadlink/test_remote.py | 120 ++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 tests/components/broadlink/test_remote.py diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 5d6c3312e51..4051f94c0d3 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -1,7 +1,7 @@ """Tests for the Broadlink integration.""" from homeassistant.components.broadlink.const import DOMAIN -from tests.async_mock import MagicMock +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry # Do not edit/remove. Adding is ok. @@ -76,6 +76,21 @@ class BroadlinkDevice: self.timeout: int = timeout self.fwversion: int = fwversion + async def setup_entry(self, hass, mock_api=None, mock_entry=None): + """Set up the device.""" + mock_api = mock_api or self.get_mock_api() + mock_entry = mock_entry or self.get_mock_entry() + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.broadlink.device.blk.gendevice", + return_value=mock_api, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + return mock_api, mock_entry + def get_mock_api(self): """Return a mock device (API).""" mock_api = MagicMock() diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py new file mode 100644 index 00000000000..941dfc4d3ce --- /dev/null +++ b/tests/components/broadlink/test_remote.py @@ -0,0 +1,120 @@ +"""Tests for Broadlink remotes.""" +from base64 import b64decode + +from homeassistant.components.broadlink.const import DOMAIN, REMOTE_DOMAIN +from homeassistant.components.remote import ( + SERVICE_SEND_COMMAND, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.entity_registry import async_entries_for_device + +from . import get_device + +from tests.async_mock import call +from tests.common import mock_device_registry, mock_registry + +REMOTE_DEVICES = ["Entrance", "Living Room", "Office", "Garage"] + +IR_PACKET = ( + "JgBGAJKVETkRORA6ERQRFBEUERQRFBE5ETkQOhAVEBUQFREUEBUQ" + "OhEUERQRORE5EBURFBA6EBUQOhE5EBUQFRA6EDoRFBEADQUAAA==" +) + + +async def test_remote_setup_works(hass): + """Test a successful setup with all remotes.""" + for device in map(get_device, REMOTE_DEVICES): + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + mock_api, mock_entry = await device.setup_entry(hass) + + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} + assert len(remotes) == 1 + + remote = remotes.pop() + assert remote.original_name == f"{device.name} Remote" + assert hass.states.get(remote.entity_id).state == STATE_ON + assert mock_api.auth.call_count == 1 + + +async def test_remote_send_command(hass): + """Test sending a command with all remotes.""" + for device in map(get_device, REMOTE_DEVICES): + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + mock_api, mock_entry = await device.setup_entry(hass) + + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} + assert len(remotes) == 1 + + remote = remotes.pop() + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, + blocking=True, + ) + + assert mock_api.send_data.call_count == 1 + assert mock_api.send_data.call_args == call(b64decode(IR_PACKET)) + assert mock_api.auth.call_count == 1 + + +async def test_remote_turn_off_turn_on(hass): + """Test we do not send commands if the remotes are off.""" + for device in map(get_device, REMOTE_DEVICES): + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + mock_api, mock_entry = await device.setup_entry(hass) + + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_entry.unique_id)}, set() + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} + assert len(remotes) == 1 + + remote = remotes.pop() + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": remote.entity_id}, + blocking=True, + ) + assert hass.states.get(remote.entity_id).state == STATE_OFF + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, + blocking=True, + ) + assert mock_api.send_data.call_count == 0 + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {"entity_id": remote.entity_id}, + blocking=True, + ) + assert hass.states.get(remote.entity_id).state == STATE_ON + + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {"entity_id": remote.entity_id, "command": "b64:" + IR_PACKET}, + blocking=True, + ) + assert mock_api.send_data.call_count == 1 + assert mock_api.send_data.call_args == call(b64decode(IR_PACKET)) + assert mock_api.auth.call_count == 1 From bfd6989c75ccd3fc3707a04662bfc04d48e760a7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 4 Sep 2020 00:04:26 +0000 Subject: [PATCH 621/862] [ci skip] Translation update --- .../components/deconz/translations/pl.json | 14 +++++------ .../components/dsmr/translations/en.json | 7 ++++++ .../homematicip_cloud/translations/bg.json | 2 +- .../homematicip_cloud/translations/ca.json | 1 + .../homematicip_cloud/translations/cs.json | 2 +- .../homematicip_cloud/translations/da.json | 2 +- .../homematicip_cloud/translations/de.json | 2 +- .../homematicip_cloud/translations/en.json | 1 + .../translations/es-419.json | 2 +- .../homematicip_cloud/translations/es.json | 2 +- .../homematicip_cloud/translations/et.json | 2 +- .../homematicip_cloud/translations/fi.json | 2 +- .../homematicip_cloud/translations/fr.json | 2 +- .../homematicip_cloud/translations/he.json | 2 +- .../homematicip_cloud/translations/hu.json | 2 +- .../homematicip_cloud/translations/id.json | 2 +- .../homematicip_cloud/translations/it.json | 2 +- .../homematicip_cloud/translations/ja.json | 2 +- .../homematicip_cloud/translations/ko.json | 2 +- .../homematicip_cloud/translations/lb.json | 2 +- .../homematicip_cloud/translations/nl.json | 2 +- .../homematicip_cloud/translations/nn.json | 2 +- .../homematicip_cloud/translations/no.json | 2 +- .../homematicip_cloud/translations/pl.json | 2 +- .../homematicip_cloud/translations/pt-BR.json | 2 +- .../homematicip_cloud/translations/pt.json | 2 +- .../homematicip_cloud/translations/ro.json | 2 +- .../homematicip_cloud/translations/ru.json | 1 + .../homematicip_cloud/translations/sl.json | 2 +- .../homematicip_cloud/translations/sv.json | 2 +- .../translations/zh-Hans.json | 2 +- .../translations/zh-Hant.json | 1 + .../components/hue/translations/pl.json | 16 ++++++------- .../components/mqtt/translations/pl.json | 12 +++++----- .../components/nzbget/translations/pl.json | 7 ++++++ .../components/onvif/translations/en.json | 8 +++---- .../components/sharkiq/translations/pl.json | 7 ++++++ .../components/shelly/translations/pl.json | 7 ++++++ .../xiaomi_aqara/translations/en.json | 2 +- .../components/yeelight/translations/no.json | 3 ++- .../yeelight/translations/zh-Hant.json | 3 ++- .../components/zha/translations/pl.json | 24 +++++++++---------- 42 files changed, 100 insertions(+), 66 deletions(-) create mode 100644 homeassistant/components/dsmr/translations/en.json create mode 100644 homeassistant/components/nzbget/translations/pl.json create mode 100644 homeassistant/components/sharkiq/translations/pl.json diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 72434f4700f..27dbc0535ce 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -41,12 +41,12 @@ "button_1": "pierwszy przycisk", "button_2": "drugi przycisk", "button_3": "trzeci przycisk", - "button_4": "czwarty przycisk", - "close": "nast\u0105pi zamkni\u0119cie", - "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", - "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", + "button_4": "czwarty", + "close": "zamknij", + "dim_down": "zmniejszenie jasno\u015bci", + "dim_up": "zwi\u0119kszenie jasno\u015bci", "left": "w lewo", - "open": "otwarcie", + "open": "otw\u00f3rz", "right": "w prawo", "side_1": "strona 1", "side_2": "strona 2", @@ -55,8 +55,8 @@ "side_5": "strona 5", "side_6": "strona 6", "top_buttons": "g\u00f3rne przyciski", - "turn_off": "nast\u0105pi wy\u0142\u0105czenie", - "turn_on": "nast\u0105pi w\u0142\u0105czenie" + "turn_off": "wy\u0142\u0105cz", + "turn_on": "w\u0142\u0105cz" }, "trigger_type": { "remote_awakened": "urz\u0105dzenie si\u0119 obudzi", diff --git a/homeassistant/components/dsmr/translations/en.json b/homeassistant/components/dsmr/translations/en.json new file mode 100644 index 00000000000..1344d2f6988 --- /dev/null +++ b/homeassistant/components/dsmr/translations/en.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/bg.json b/homeassistant/components/homematicip_cloud/translations/bg.json index dd04e1e8250..a666c5aef4e 100644 --- a/homeassistant/components/homematicip_cloud/translations/bg.json +++ b/homeassistant/components/homematicip_cloud/translations/bg.json @@ -6,7 +6,7 @@ "unknown": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430." }, "error": { - "invalid_pin": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u041f\u0418\u041d \u043a\u043e\u0434, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", + "invalid_sgtin_or_pin": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u041f\u0418\u041d \u043a\u043e\u0434, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", "press_the_button": "\u041c\u043e\u043b\u044f, \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0441\u0438\u043d\u0438\u044f \u0431\u0443\u0442\u043e\u043d.", "register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", "timeout_button": "\u0421\u0438\u043d\u0438\u044f \u0431\u0443\u0442\u043e\u043d \u043d\u0435 \u0431\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." diff --git a/homeassistant/components/homematicip_cloud/translations/ca.json b/homeassistant/components/homematicip_cloud/translations/ca.json index 377b6e53a40..fe64ea49e83 100644 --- a/homeassistant/components/homematicip_cloud/translations/ca.json +++ b/homeassistant/components/homematicip_cloud/translations/ca.json @@ -7,6 +7,7 @@ }, "error": { "invalid_pin": "Codi PIN inv\u00e0lid, torna-ho a provar.", + "invalid_sgtin_or_pin": "Codi PIN inv\u00e0lid, torna-ho a provar.", "press_the_button": "Si us plau, prem el bot\u00f3 blau.", "register_failed": "Error al registrar, torna-ho a provar.", "timeout_button": "El temps d'espera m\u00e0xim per pr\u00e9mer el bot\u00f3 blau s'ha esgotat, torna-ho a provar." diff --git a/homeassistant/components/homematicip_cloud/translations/cs.json b/homeassistant/components/homematicip_cloud/translations/cs.json index efb73dd6e7f..36b6cd00e98 100644 --- a/homeassistant/components/homematicip_cloud/translations/cs.json +++ b/homeassistant/components/homematicip_cloud/translations/cs.json @@ -6,7 +6,7 @@ "unknown": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b" }, "error": { - "invalid_pin": "Neplatn\u00fd k\u00f3d PIN, zkuste to znovu.", + "invalid_sgtin_or_pin": "Neplatn\u00fd k\u00f3d PIN, zkuste to znovu.", "press_the_button": "Stiskn\u011bte modr\u00e9 tla\u010d\u00edtko.", "register_failed": "Registrace se nezda\u0159ila, zkuste to znovu.", "timeout_button": "\u010casov\u00fd limit stisknut\u00ed modr\u00e9ho tla\u010d\u00edtka vypr\u0161el. Zkuste to znovu." diff --git a/homeassistant/components/homematicip_cloud/translations/da.json b/homeassistant/components/homematicip_cloud/translations/da.json index a520aef37d7..5a8248d6cef 100644 --- a/homeassistant/components/homematicip_cloud/translations/da.json +++ b/homeassistant/components/homematicip_cloud/translations/da.json @@ -6,7 +6,7 @@ "unknown": "Ukendt fejl opstod" }, "error": { - "invalid_pin": "Ugyldig PIN, pr\u00f8v igen.", + "invalid_sgtin_or_pin": "Ugyldig PIN, pr\u00f8v igen.", "press_the_button": "Tryk venligst p\u00e5 den bl\u00e5 knap.", "register_failed": "Fejl ved registrering, pr\u00f8v venligst igen.", "timeout_button": "Tryk p\u00e5 bl\u00e5 knap timeout, pr\u00f8v venligst igen." diff --git a/homeassistant/components/homematicip_cloud/translations/de.json b/homeassistant/components/homematicip_cloud/translations/de.json index f3ec84fe3af..c421620fd98 100644 --- a/homeassistant/components/homematicip_cloud/translations/de.json +++ b/homeassistant/components/homematicip_cloud/translations/de.json @@ -6,7 +6,7 @@ "unknown": "Ein unbekannter Fehler ist aufgetreten." }, "error": { - "invalid_pin": "Ung\u00fcltige PIN, bitte versuche es erneut.", + "invalid_sgtin_or_pin": "Ung\u00fcltige PIN, bitte versuche es erneut.", "press_the_button": "Bitte dr\u00fccke die blaue Taste.", "register_failed": "Registrierung fehlgeschlagen, bitte versuche es erneut.", "timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuche es erneut." diff --git a/homeassistant/components/homematicip_cloud/translations/en.json b/homeassistant/components/homematicip_cloud/translations/en.json index 74d42fc7bc4..97cfd1e3f4e 100644 --- a/homeassistant/components/homematicip_cloud/translations/en.json +++ b/homeassistant/components/homematicip_cloud/translations/en.json @@ -6,6 +6,7 @@ "unknown": "Unknown error occurred." }, "error": { + "invalid_pin": "Invalid PIN, please try again.", "invalid_sgtin_or_pin": "Invalid SGTIN or PIN, please try again.", "press_the_button": "Please press the blue button.", "register_failed": "Failed to register, please try again.", diff --git a/homeassistant/components/homematicip_cloud/translations/es-419.json b/homeassistant/components/homematicip_cloud/translations/es-419.json index 1b743f1c51f..c2dc90ff867 100644 --- a/homeassistant/components/homematicip_cloud/translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/translations/es-419.json @@ -6,7 +6,7 @@ "unknown": "Se produjo un error desconocido." }, "error": { - "invalid_pin": "PIN no v\u00e1lido, por favor intente de nuevo.", + "invalid_sgtin_or_pin": "PIN no v\u00e1lido, por favor intente de nuevo.", "press_the_button": "Por favor, presione el bot\u00f3n azul.", "register_failed": "No se pudo registrar, por favor intente de nuevo.", "timeout_button": "Tiempo de espera del bot\u00f3n azul, intente nuevamente." diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json index b1d1ea64e3f..b5877fe09ce 100644 --- a/homeassistant/components/homematicip_cloud/translations/es.json +++ b/homeassistant/components/homematicip_cloud/translations/es.json @@ -6,7 +6,7 @@ "unknown": "Se ha producido un error desconocido." }, "error": { - "invalid_pin": "PIN no v\u00e1lido, por favor int\u00e9ntalo de nuevo.", + "invalid_sgtin_or_pin": "PIN no v\u00e1lido, por favor int\u00e9ntalo de nuevo.", "press_the_button": "Por favor, pulsa el bot\u00f3n azul", "register_failed": "No se pudo registrar, por favor intentelo de nuevo.", "timeout_button": "Tiempo de espera agotado desde que se apret\u00f3 el bot\u00f3n azul, por favor, int\u00e9ntalo de nuevo." diff --git a/homeassistant/components/homematicip_cloud/translations/et.json b/homeassistant/components/homematicip_cloud/translations/et.json index 08bd179b4f9..92f07d401e6 100644 --- a/homeassistant/components/homematicip_cloud/translations/et.json +++ b/homeassistant/components/homematicip_cloud/translations/et.json @@ -1,7 +1,7 @@ { "config": { "error": { - "invalid_pin": "Vale PIN, palun proovige uuesti" + "invalid_sgtin_or_pin": "Vale PIN, palun proovige uuesti" }, "step": { "init": { diff --git a/homeassistant/components/homematicip_cloud/translations/fi.json b/homeassistant/components/homematicip_cloud/translations/fi.json index fe5cc461992..9fcaacf4ba1 100644 --- a/homeassistant/components/homematicip_cloud/translations/fi.json +++ b/homeassistant/components/homematicip_cloud/translations/fi.json @@ -4,7 +4,7 @@ "unknown": "Tapahtui tuntematon virhe." }, "error": { - "invalid_pin": "Virheellinen PIN-koodi, yrit\u00e4 uudelleen.", + "invalid_sgtin_or_pin": "Virheellinen PIN-koodi, yrit\u00e4 uudelleen.", "press_the_button": "Paina sinist\u00e4 painiketta." } } diff --git a/homeassistant/components/homematicip_cloud/translations/fr.json b/homeassistant/components/homematicip_cloud/translations/fr.json index 212a6d27796..9bcb8be7e0e 100644 --- a/homeassistant/components/homematicip_cloud/translations/fr.json +++ b/homeassistant/components/homematicip_cloud/translations/fr.json @@ -6,7 +6,7 @@ "unknown": "Une erreur inconnue s'est produite." }, "error": { - "invalid_pin": "Code PIN invalide, veuillez r\u00e9essayer.", + "invalid_sgtin_or_pin": "Code PIN invalide, veuillez r\u00e9essayer.", "press_the_button": "Veuillez appuyer sur le bouton bleu.", "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer.", "timeout_button": "D\u00e9lai d'attente expir\u00e9, veuillez r\u00e9\u00e9ssayer." diff --git a/homeassistant/components/homematicip_cloud/translations/he.json b/homeassistant/components/homematicip_cloud/translations/he.json index 9e650e132fe..f07db79a1c5 100644 --- a/homeassistant/components/homematicip_cloud/translations/he.json +++ b/homeassistant/components/homematicip_cloud/translations/he.json @@ -6,7 +6,7 @@ "unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4." }, "error": { - "invalid_pin": "PIN \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "invalid_sgtin_or_pin": "PIN \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", "press_the_button": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc.", "register_failed": "\u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05e0\u05db\u05e9\u05dc, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", "timeout_button": "\u05e2\u05d1\u05e8 \u05d4\u05d6\u05de\u05df \u05d4\u05e7\u05e6\u05d5\u05d1 \u05dc\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1" diff --git a/homeassistant/components/homematicip_cloud/translations/hu.json b/homeassistant/components/homematicip_cloud/translations/hu.json index a410d9e28b6..1ae318c45ad 100644 --- a/homeassistant/components/homematicip_cloud/translations/hu.json +++ b/homeassistant/components/homematicip_cloud/translations/hu.json @@ -6,7 +6,7 @@ "unknown": "Unknown error occurred." }, "error": { - "invalid_pin": "\u00c9rv\u00e9nytelen PIN, pr\u00f3b\u00e1lkozz \u00fajra.", + "invalid_sgtin_or_pin": "\u00c9rv\u00e9nytelen PIN, pr\u00f3b\u00e1lkozz \u00fajra.", "press_the_button": "Nyomd meg a k\u00e9k gombot.", "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, pr\u00f3b\u00e1ld \u00fajra.", "timeout_button": "K\u00e9k gomb megnyom\u00e1s\u00e1nak id\u0151t\u00fall\u00e9p\u00e9se, pr\u00f3b\u00e1lkozz \u00fajra." diff --git a/homeassistant/components/homematicip_cloud/translations/id.json b/homeassistant/components/homematicip_cloud/translations/id.json index 8c99cddf6fb..43525955c36 100644 --- a/homeassistant/components/homematicip_cloud/translations/id.json +++ b/homeassistant/components/homematicip_cloud/translations/id.json @@ -6,7 +6,7 @@ "unknown": "Kesalahan tidak dikenal terjadi." }, "error": { - "invalid_pin": "PIN tidak valid, silakan coba lagi.", + "invalid_sgtin_or_pin": "PIN tidak valid, silakan coba lagi.", "press_the_button": "Silakan tekan tombol biru.", "register_failed": "Gagal mendaftar, silakan coba lagi.", "timeout_button": "Batas waktu tekan tombol biru berakhir, silakan coba lagi." diff --git a/homeassistant/components/homematicip_cloud/translations/it.json b/homeassistant/components/homematicip_cloud/translations/it.json index d00f082d2ed..9be01273fc1 100644 --- a/homeassistant/components/homematicip_cloud/translations/it.json +++ b/homeassistant/components/homematicip_cloud/translations/it.json @@ -6,7 +6,7 @@ "unknown": "Si \u00e8 verificato un errore sconosciuto." }, "error": { - "invalid_pin": "PIN non valido, riprova.", + "invalid_sgtin_or_pin": "PIN non valido, riprova.", "press_the_button": "Si prega di premere il pulsante blu.", "register_failed": "Registrazione fallita, si prega di riprovare.", "timeout_button": "Timeout della pressione del pulsante blu, riprovare." diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index 6a03f3ec76b..b26b247a66c 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -5,7 +5,7 @@ "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "error": { - "invalid_pin": "PIN\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "invalid_sgtin_or_pin": "PIN\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "press_the_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } diff --git a/homeassistant/components/homematicip_cloud/translations/ko.json b/homeassistant/components/homematicip_cloud/translations/ko.json index 54c129bec6d..b85b8ac00b1 100644 --- a/homeassistant/components/homematicip_cloud/translations/ko.json +++ b/homeassistant/components/homematicip_cloud/translations/ko.json @@ -6,7 +6,7 @@ "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "invalid_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_sgtin_or_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "press_the_button": "\ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", "register_failed": "\ub4f1\ub85d\uc5d0 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "timeout_button": "\uc815\ud574\uc9c4 \uc2dc\uac04\ub0b4\uc5d0 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uc744 \ub20c\ub974\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." diff --git a/homeassistant/components/homematicip_cloud/translations/lb.json b/homeassistant/components/homematicip_cloud/translations/lb.json index 393a0689bff..80892a3e282 100644 --- a/homeassistant/components/homematicip_cloud/translations/lb.json +++ b/homeassistant/components/homematicip_cloud/translations/lb.json @@ -6,7 +6,7 @@ "unknown": "Onbekannten Feeler opgetrueden" }, "error": { - "invalid_pin": "Ong\u00ebltege Pin, prob\u00e9iert w.e.g. nach emol.", + "invalid_sgtin_or_pin": "Ong\u00ebltege Pin, prob\u00e9iert w.e.g. nach emol.", "press_the_button": "Dr\u00e9ckt w.e.g. de bloe Kn\u00e4ppchen.", "register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9iert w.e.g. nach emol.", "timeout_button": "Z\u00e4itiwwerschreidung beim dr\u00e9cken vum bloe Kn\u00e4ppchen, prob\u00e9iert w.e.g. nach emol." diff --git a/homeassistant/components/homematicip_cloud/translations/nl.json b/homeassistant/components/homematicip_cloud/translations/nl.json index af5b37773b0..7127b5c5aae 100644 --- a/homeassistant/components/homematicip_cloud/translations/nl.json +++ b/homeassistant/components/homematicip_cloud/translations/nl.json @@ -6,7 +6,7 @@ "unknown": "Er is een onbekende fout opgetreden." }, "error": { - "invalid_pin": "Ongeldige PIN-code, probeer het nogmaals.", + "invalid_sgtin_or_pin": "Ongeldige PIN-code, probeer het nogmaals.", "press_the_button": "Druk op de blauwe knop.", "register_failed": "Kan niet registreren, gelieve opnieuw te proberen.", "timeout_button": "Blauwe knop druk op timeout, probeer het opnieuw." diff --git a/homeassistant/components/homematicip_cloud/translations/nn.json b/homeassistant/components/homematicip_cloud/translations/nn.json index c4154e74897..6c7fecda17e 100644 --- a/homeassistant/components/homematicip_cloud/translations/nn.json +++ b/homeassistant/components/homematicip_cloud/translations/nn.json @@ -6,7 +6,7 @@ "unknown": "Det hende ein ukjent feil." }, "error": { - "invalid_pin": "Ugyldig PIN. Pr\u00f8v igjen.", + "invalid_sgtin_or_pin": "Ugyldig PIN. Pr\u00f8v igjen.", "press_the_button": "Ver vennleg og trykk p\u00e5 den bl\u00e5 knappen.", "register_failed": "Kunne ikkje registrere. Pr\u00f8v igjen.", "timeout_button": "TIda gjekk ut for \u00e5 trykke p\u00e5 den bl\u00e5 knappen. Ver vennleg og pr\u00f8v igjen." diff --git a/homeassistant/components/homematicip_cloud/translations/no.json b/homeassistant/components/homematicip_cloud/translations/no.json index e6a506a7768..1879860f7a7 100644 --- a/homeassistant/components/homematicip_cloud/translations/no.json +++ b/homeassistant/components/homematicip_cloud/translations/no.json @@ -6,7 +6,7 @@ "unknown": "Ukjent feil oppstod." }, "error": { - "invalid_pin": "Ugyldig PIN kode, pr\u00f8v igjen.", + "invalid_sgtin_or_pin": "Ugyldig PIN kode, pr\u00f8v igjen.", "press_the_button": "Vennligst trykk p\u00e5 den bl\u00e5 knappen.", "register_failed": "Kunne ikke registrere, vennligst pr\u00f8v igjen.", "timeout_button": "Bl\u00e5 knapp-trykk tok for lang tid, vennligst pr\u00f8v igjen." diff --git a/homeassistant/components/homematicip_cloud/translations/pl.json b/homeassistant/components/homematicip_cloud/translations/pl.json index d51af1e32ea..9071c2072ae 100644 --- a/homeassistant/components/homematicip_cloud/translations/pl.json +++ b/homeassistant/components/homematicip_cloud/translations/pl.json @@ -6,7 +6,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { - "invalid_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie.", + "invalid_sgtin_or_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie.", "press_the_button": "Prosz\u0119 nacisn\u0105\u0107 niebieski przycisk.", "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107, spr\u00f3buj ponownie.", "timeout_button": "Oczekiwania na naci\u015bni\u0119cie niebieskiego przycisku zako\u0144czone, spr\u00f3buj ponownie." diff --git a/homeassistant/components/homematicip_cloud/translations/pt-BR.json b/homeassistant/components/homematicip_cloud/translations/pt-BR.json index 5f7fc81bd9d..c19678ad0c4 100644 --- a/homeassistant/components/homematicip_cloud/translations/pt-BR.json +++ b/homeassistant/components/homematicip_cloud/translations/pt-BR.json @@ -6,7 +6,7 @@ "unknown": "Ocorreu um erro desconhecido." }, "error": { - "invalid_pin": "PIN inv\u00e1lido, por favor tente novamente.", + "invalid_sgtin_or_pin": "PIN inv\u00e1lido, por favor tente novamente.", "press_the_button": "Por favor, pressione o bot\u00e3o azul.", "register_failed": "Falha ao registrar, por favor tente novamente.", "timeout_button": "Tempo para pressionar o Bot\u00e3o Azul expirou, por favor tente novamente." diff --git a/homeassistant/components/homematicip_cloud/translations/pt.json b/homeassistant/components/homematicip_cloud/translations/pt.json index 649530de53c..645ba242561 100644 --- a/homeassistant/components/homematicip_cloud/translations/pt.json +++ b/homeassistant/components/homematicip_cloud/translations/pt.json @@ -6,7 +6,7 @@ "unknown": "Ocorreu um erro desconhecido." }, "error": { - "invalid_pin": "PIN inv\u00e1lido. Por favor, tente novamente.", + "invalid_sgtin_or_pin": "PIN inv\u00e1lido. Por favor, tente novamente.", "press_the_button": "Por favor, pressione o bot\u00e3o azul.", "register_failed": "Falha ao registar. Por favor, tente novamente.", "timeout_button": "Tempo limite ultrapassado para carregar bot\u00e3o azul, por favor, tente de novo." diff --git a/homeassistant/components/homematicip_cloud/translations/ro.json b/homeassistant/components/homematicip_cloud/translations/ro.json index a5399e7e68c..2ff7fd81ddb 100644 --- a/homeassistant/components/homematicip_cloud/translations/ro.json +++ b/homeassistant/components/homematicip_cloud/translations/ro.json @@ -5,7 +5,7 @@ "unknown": "Sa produs o eroare necunoscut\u0103." }, "error": { - "invalid_pin": "Cod PIN invalid, \u00eencerca\u021bi din nou.", + "invalid_sgtin_or_pin": "Cod PIN invalid, \u00eencerca\u021bi din nou.", "press_the_button": "V\u0103 rug\u0103m s\u0103 ap\u0103sa\u021bi butonul albastru." }, "step": { diff --git a/homeassistant/components/homematicip_cloud/translations/ru.json b/homeassistant/components/homematicip_cloud/translations/ru.json index 58ee71e722b..df87a5e42af 100644 --- a/homeassistant/components/homematicip_cloud/translations/ru.json +++ b/homeassistant/components/homematicip_cloud/translations/ru.json @@ -7,6 +7,7 @@ }, "error": { "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", + "invalid_sgtin_or_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "press_the_button": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443.", "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430." diff --git a/homeassistant/components/homematicip_cloud/translations/sl.json b/homeassistant/components/homematicip_cloud/translations/sl.json index a0179dfc90d..3ced2a1a9bd 100644 --- a/homeassistant/components/homematicip_cloud/translations/sl.json +++ b/homeassistant/components/homematicip_cloud/translations/sl.json @@ -6,7 +6,7 @@ "unknown": "Pri\u0161lo je do neznane napake" }, "error": { - "invalid_pin": "Neveljavna koda PIN, poskusite znova.", + "invalid_sgtin_or_pin": "Neveljavna koda PIN, poskusite znova.", "press_the_button": "Prosimo, pritisnite modri gumb.", "register_failed": "Registracija ni uspela, poskusite znova", "timeout_button": "Potekla je \u010dasovna omejitev za pritisk modrega gumba, poskusite znova." diff --git a/homeassistant/components/homematicip_cloud/translations/sv.json b/homeassistant/components/homematicip_cloud/translations/sv.json index 85c71ca3fad..49078bc22da 100644 --- a/homeassistant/components/homematicip_cloud/translations/sv.json +++ b/homeassistant/components/homematicip_cloud/translations/sv.json @@ -6,7 +6,7 @@ "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" }, "error": { - "invalid_pin": "Ogiltig PIN-kod, f\u00f6rs\u00f6k igen.", + "invalid_sgtin_or_pin": "Ogiltig PIN-kod, f\u00f6rs\u00f6k igen.", "press_the_button": "V\u00e4nligen tryck p\u00e5 den bl\u00e5 knappen.", "register_failed": "Misslyckades med att registrera, f\u00f6rs\u00f6k igen.", "timeout_button": "Bl\u00e5 knapptryckning timeout, f\u00f6rs\u00f6k igen." diff --git a/homeassistant/components/homematicip_cloud/translations/zh-Hans.json b/homeassistant/components/homematicip_cloud/translations/zh-Hans.json index 02781ba48f4..a1d87d0d196 100644 --- a/homeassistant/components/homematicip_cloud/translations/zh-Hans.json +++ b/homeassistant/components/homematicip_cloud/translations/zh-Hans.json @@ -6,7 +6,7 @@ "unknown": "\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002" }, "error": { - "invalid_pin": "PIN \u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002", + "invalid_sgtin_or_pin": "PIN \u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002", "press_the_button": "\u8bf7\u6309\u4e0b\u84dd\u8272\u6309\u94ae\u3002", "register_failed": "\u6ce8\u518c\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5", "timeout_button": "\u6309\u4e0b\u84dd\u8272\u6309\u94ae\u8d85\u65f6\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002" diff --git a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json index e457ce1631c..d339d3ec6a8 100644 --- a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json @@ -7,6 +7,7 @@ }, "error": { "invalid_pin": "PIN \u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_sgtin_or_pin": "SGTIN \u6216 PIN \u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "press_the_button": "\u8acb\u6309\u4e0b\u85cd\u8272\u6309\u9215\u3002", "register_failed": "\u8a3b\u518a\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "timeout_button": "\u85cd\u8272\u6309\u9215\u903e\u6642\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index cbfd5ecfb5d..02dad0c3e52 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -35,14 +35,14 @@ }, "device_automation": { "trigger_subtype": { - "button_1": "pierwszy przycisk", - "button_2": "drugi przycisk", - "button_3": "trzeci przycisk", - "button_4": "czwarty przycisk", - "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", - "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", - "double_buttons_1_3": "przyciski pierwszy i trzeci", - "double_buttons_2_4": "przyciski drugi i czwarty", + "button_1": "pierwszy", + "button_2": "drugi", + "button_3": "trzeci", + "button_4": "czwarty", + "dim_down": "zmniejszenie jasno\u015bci", + "dim_up": "zwi\u0119kszenie jasno\u015bci", + "double_buttons_1_3": "pierwszy i trzeci", + "double_buttons_2_4": "drugi i czwarty", "turn_off": "wy\u0142\u0105cznik", "turn_on": "w\u0142\u0105cznik" }, diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 8c90db3e773..92c6d49e677 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -28,12 +28,12 @@ }, "device_automation": { "trigger_subtype": { - "button_1": "pierwszy przycisk", - "button_2": "drugi przycisk", - "button_3": "trzeci przycisk", - "button_4": "czwarty przycisk", - "button_5": "pi\u0105ty przycisk", - "button_6": "sz\u00f3sty przycisk", + "button_1": "pierwszy", + "button_2": "drugi", + "button_3": "trzeci", + "button_4": "czwarty", + "button_5": "pi\u0105ty", + "button_6": "sz\u00f3sty", "turn_off": "nast\u0105pi wy\u0142\u0105czenie", "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, diff --git a/homeassistant/components/nzbget/translations/pl.json b/homeassistant/components/nzbget/translations/pl.json new file mode 100644 index 00000000000..599616a8b15 --- /dev/null +++ b/homeassistant/components/nzbget/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json index df47900fbc5..a20b1fcb7e4 100644 --- a/homeassistant/components/onvif/translations/en.json +++ b/homeassistant/components/onvif/translations/en.json @@ -13,8 +13,8 @@ "step": { "auth": { "data": { - "password": "[%key:common::config_flow::data::password%]", - "username": "[%key:common::config_flow::data::username%]" + "password": "Password", + "username": "Username" }, "title": "Configure authentication" }, @@ -33,9 +33,9 @@ }, "manual_input": { "data": { - "host": "[%key:common::config_flow::data::host%]", + "host": "Host", "name": "Name", - "port": "[%key:common::config_flow::data::port%]" + "port": "Port" }, "title": "Configure ONVIF device" }, diff --git a/homeassistant/components/sharkiq/translations/pl.json b/homeassistant/components/sharkiq/translations/pl.json new file mode 100644 index 00000000000..599616a8b15 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index 1bf40fec860..77a7f045671 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -6,6 +6,7 @@ "error": { "auth_not_supported": "Urz\u0105dzenia Shelly wymagaj\u0105ce uwierzytelnienia nie s\u0105 obecnie obs\u0142ugiwane.", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "Nieoczekiwany b\u0142\u0105d." }, "flow_title": "Shelly: {name}", @@ -13,6 +14,12 @@ "confirm_discovery": { "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?" }, + "credentials": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json index fe77ae51a63..4cdc7ee497a 100644 --- a/homeassistant/components/xiaomi_aqara/translations/en.json +++ b/homeassistant/components/xiaomi_aqara/translations/en.json @@ -41,4 +41,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/no.json b/homeassistant/components/yeelight/translations/no.json index 07f0b7be05c..365bdb7ba0f 100644 --- a/homeassistant/components/yeelight/translations/no.json +++ b/homeassistant/components/yeelight/translations/no.json @@ -15,9 +15,10 @@ }, "user": { "data": { + "host": "Vert", "ip_address": "IP adresse" }, - "description": "Hvis du lar IP-adressen st\u00e5 tom, brukes oppdagelsen til \u00e5 finne enheter." + "description": "Hvis du lar verten st\u00e5 tom, brukes oppdagelsen til \u00e5 finne enheter." } } }, diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index 4f5f766e3e2..9c41f3c3be6 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -15,9 +15,10 @@ }, "user": { "data": { + "host": "\u4e3b\u6a5f\u7aef", "ip_address": "IP \u4f4d\u5740" }, - "description": "\u5047\u5982 IP \u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u8a2d\u5099\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u8a2d\u5099\u3002" } } }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index b9780d8427f..b2089e0dad8 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -39,15 +39,15 @@ }, "trigger_subtype": { "both_buttons": "oba przyciski", - "button_1": "pierwszy przycisk", - "button_2": "drugi przycisk", - "button_3": "trzeci przycisk", - "button_4": "czwarty przycisk", - "button_5": "pi\u0105ty przycisk", - "button_6": "sz\u00f3sty przycisk", - "close": "nast\u0105pi zamkni\u0119cie", - "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", - "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", + "button_1": "pierwszy", + "button_2": "drugi", + "button_3": "trzeci", + "button_4": "czwarty", + "button_5": "pi\u0105ty", + "button_6": "sz\u00f3sty", + "close": "zamknij", + "dim_down": "zmniejszenie jasno\u015bci", + "dim_up": "zwi\u0119kszenie jasno\u015bci", "face_1": "z aktywowan\u0105 twarz\u0105 1", "face_2": "z aktywowan\u0105 twarz\u0105 2", "face_3": "z aktywowan\u0105 twarz\u0105 3", @@ -56,10 +56,10 @@ "face_6": "z aktywowan\u0105 twarz\u0105 6", "face_any": "z dowoln\u0105 twarz\u0105 aktywowan\u0105", "left": "w lewo", - "open": "otwarcie", + "open": "otw\u00f3rz", "right": "w prawo", - "turn_off": "nast\u0105pi wy\u0142\u0105czenie", - "turn_on": "nast\u0105pi w\u0142\u0105czenie" + "turn_off": "wy\u0142\u0105cz", + "turn_on": "w\u0142\u0105cz" }, "trigger_type": { "device_dropped": "nast\u0105pi upadek urz\u0105dzenia", From 1034e4ec5090e9c5673fdc18afd06c04173395c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Sep 2020 19:54:48 -0500 Subject: [PATCH 622/862] Bump nexia to 0.9.4 (#39634) Fix for type missing in the json response --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index e86d2072db8..1a4d6c74e84 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia", - "requirements": ["nexia==0.9.3"], + "requirements": ["nexia==0.9.4"], "codeowners": ["@ryannazaretian", "@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 534a236ac33..495e4b9cdd3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -958,7 +958,7 @@ netdisco==2.8.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.3 +nexia==0.9.4 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 206e6448bef..aa07cfea08d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -457,7 +457,7 @@ nessclient==0.9.15 netdisco==2.8.2 # homeassistant.components.nexia -nexia==0.9.3 +nexia==0.9.4 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 From 48cfbf8662ec1b05319fbc3cfcf6f5fe3bf89fb9 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 3 Sep 2020 20:01:15 -0500 Subject: [PATCH 623/862] Improve broadlink sensor tests (#39632) --- tests/components/broadlink/test_sensors.py | 57 +++------------------- 1 file changed, 8 insertions(+), 49 deletions(-) diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index 132ab31d8be..a7d6a304654 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -4,18 +4,9 @@ from homeassistant.helpers.entity_registry import async_entries_for_device from . import get_device -from tests.async_mock import patch from tests.common import mock_device_registry, mock_registry -def _patch_broadlink_gendevice(return_value): - """Patch the broadlink gendevice method.""" - return patch( - "homeassistant.components.broadlink.device.blk.gendevice", - return_value=return_value, - ) - - async def test_a1_sensor_setup(hass): """Test a successful e-Sensor setup.""" device = get_device("Bedroom") @@ -27,15 +18,11 @@ async def test_a1_sensor_setup(hass): "light": 2, "noise": 1, } - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with _patch_broadlink_gendevice(return_value=mock_api): - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors_raw.call_count == 1 device_entry = device_registry.async_get_device( @@ -69,15 +56,11 @@ async def test_a1_sensor_update(hass): "light": 2, "noise": 1, } - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with _patch_broadlink_gendevice(return_value=mock_api): - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) device_entry = device_registry.async_get_device( {(DOMAIN, mock_entry.unique_id)}, set() @@ -116,15 +99,11 @@ async def test_rm_pro_sensor_setup(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.check_sensors.return_value = {"temperature": 18.2} - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with _patch_broadlink_gendevice(return_value=mock_api): - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device( @@ -146,15 +125,11 @@ async def test_rm_pro_sensor_update(hass): device = get_device("Office") mock_api = device.get_mock_api() mock_api.check_sensors.return_value = {"temperature": 25.7} - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with _patch_broadlink_gendevice(return_value=mock_api): - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) device_entry = device_registry.async_get_device( {(DOMAIN, mock_entry.unique_id)}, set() @@ -181,15 +156,11 @@ async def test_rm_mini3_no_sensor(hass): device = get_device("Entrance") mock_api = device.get_mock_api() mock_api.check_sensors.return_value = {"temperature": 0} - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with _patch_broadlink_gendevice(return_value=mock_api): - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count <= 1 device_entry = device_registry.async_get_device( @@ -205,15 +176,11 @@ async def test_rm4_pro_hts2_sensor_setup(hass): device = get_device("Garage") mock_api = device.get_mock_api() mock_api.check_sensors.return_value = {"temperature": 22.5, "humidity": 43.7} - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with _patch_broadlink_gendevice(return_value=mock_api): - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device( @@ -238,15 +205,11 @@ async def test_rm4_pro_hts2_sensor_update(hass): device = get_device("Garage") mock_api = device.get_mock_api() mock_api.check_sensors.return_value = {"temperature": 16.7, "humidity": 34.1} - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with _patch_broadlink_gendevice(return_value=mock_api): - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) device_entry = device_registry.async_get_device( {(DOMAIN, mock_entry.unique_id)}, set() @@ -276,15 +239,11 @@ async def test_rm4_pro_no_sensor(hass): device = get_device("Garage") mock_api = device.get_mock_api() mock_api.check_sensors.return_value = {"temperature": 0, "humidity": 0} - mock_entry = device.get_mock_entry() - mock_entry.add_to_hass(hass) device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) - with _patch_broadlink_gendevice(return_value=mock_api): - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count <= 1 device_entry = device_registry.async_get_device( From eb7742ea7cc90ab9b00d32e138015f1781cd85e1 Mon Sep 17 00:00:00 2001 From: Angelo Gagliano <25516409+TheGardenMonkey@users.noreply.github.com> Date: Thu, 3 Sep 2020 22:05:37 -0400 Subject: [PATCH 624/862] Add support for VeSync Fans (#36132) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + CODEOWNERS | 2 +- homeassistant/components/vesync/__init__.py | 44 +++++-- homeassistant/components/vesync/common.py | 13 +- homeassistant/components/vesync/const.py | 1 + homeassistant/components/vesync/fan.py | 117 ++++++++++++++++++ homeassistant/components/vesync/manifest.json | 14 ++- homeassistant/components/vesync/switch.py | 14 ++- 8 files changed, 181 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/vesync/fan.py diff --git a/.coveragerc b/.coveragerc index fdfb25be56b..0730843ccb6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -938,6 +938,7 @@ omit = homeassistant/components/vesync/__init__.py homeassistant/components/vesync/common.py homeassistant/components/vesync/const.py + homeassistant/components/vesync/fan.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* diff --git a/CODEOWNERS b/CODEOWNERS index ddd36cc7da8..a093bc722be 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -464,7 +464,7 @@ homeassistant/components/velux/* @Julius2342 homeassistant/components/vera/* @vangorra homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff -homeassistant/components/vesync/* @markperdue @webdjoe +homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey homeassistant/components/vicare/* @oischinger homeassistant/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 0f905b8d7ef..94a0d5c2f25 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -1,4 +1,5 @@ -"""Etekcity VeSync integration.""" +"""VeSync integration.""" +import asyncio import logging from pyvesync import VeSync @@ -16,10 +17,13 @@ from .const import ( SERVICE_UPDATE_DEVS, VS_DISCOVERY, VS_DISPATCHERS, + VS_FANS, VS_MANAGER, VS_SWITCHES, ) +PLATFORMS = ["switch", "fan"] + _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( @@ -80,6 +84,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN][VS_MANAGER] = manager switches = hass.data[DOMAIN][VS_SWITCHES] = [] + fans = hass.data[DOMAIN][VS_FANS] = [] hass.data[DOMAIN][VS_DISPATCHERS] = [] @@ -87,13 +92,19 @@ async def async_setup_entry(hass, config_entry): switches.extend(device_dict[VS_SWITCHES]) hass.async_create_task(forward_setup(config_entry, "switch")) + if device_dict[VS_FANS]: + fans.extend(device_dict[VS_FANS]) + hass.async_create_task(forward_setup(config_entry, "fan")) + async def async_new_device_discovery(service): """Discover if new devices should be added.""" manager = hass.data[DOMAIN][VS_MANAGER] switches = hass.data[DOMAIN][VS_SWITCHES] + fans = hass.data[DOMAIN][VS_FANS] dev_dict = await async_process_devices(hass, manager) switch_devs = dev_dict.get(VS_SWITCHES, []) + fan_devs = dev_dict.get(VS_FANS, []) switch_set = set(switch_devs) new_switches = list(switch_set.difference(switches)) @@ -105,6 +116,16 @@ async def async_setup_entry(hass, config_entry): switches.extend(new_switches) hass.async_create_task(forward_setup(config_entry, "switch")) + fan_set = set(fan_devs) + new_fans = list(fan_set.difference(fans)) + if new_fans and fans: + fans.extend(new_fans) + async_dispatcher_send(hass, VS_DISCOVERY.format(VS_FANS), new_fans) + return + if new_fans and not fans: + fans.extend(new_fans) + hass.async_create_task(forward_setup(config_entry, "fan")) + hass.services.async_register( DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery ) @@ -114,14 +135,15 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, entry): """Unload a config entry.""" - forward_unload = hass.config_entries.async_forward_entry_unload - remove_switches = False - if hass.data[DOMAIN][VS_SWITCHES]: - remove_switches = await forward_unload(entry, "switch") + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) - if remove_switches: - hass.services.async_remove(DOMAIN, SERVICE_UPDATE_DEVS) - del hass.data[DOMAIN] - return True - - return False + return unload_ok diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index d2ffa5281e9..42e3516f085 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -3,7 +3,7 @@ import logging from homeassistant.helpers.entity import ToggleEntity -from .const import VS_SWITCHES +from .const import VS_FANS, VS_SWITCHES _LOGGER = logging.getLogger(__name__) @@ -12,9 +12,14 @@ async def async_process_devices(hass, manager): """Assign devices to proper component.""" devices = {} devices[VS_SWITCHES] = [] + devices[VS_FANS] = [] await hass.async_add_executor_job(manager.update) + if manager.fans: + devices[VS_FANS].extend(manager.fans) + _LOGGER.info("%d VeSync fans found", len(manager.fans)) + if manager.outlets: devices[VS_SWITCHES].extend(manager.outlets) _LOGGER.info("%d VeSync outlets found", len(manager.outlets)) @@ -49,7 +54,7 @@ class VeSyncDevice(ToggleEntity): @property def is_on(self): - """Return True if switch is on.""" + """Return True if device is on.""" return self.device.device_status == "on" @property @@ -57,10 +62,6 @@ class VeSyncDevice(ToggleEntity): """Return True if device is available.""" return self.device.connection_status == "online" - def turn_on(self, **kwargs): - """Turn the device on.""" - self.device.turn_on() - def turn_off(self, **kwargs): """Turn the device off.""" self.device.turn_off() diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index d65edc949c7..9923ab94ecf 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -6,4 +6,5 @@ VS_DISCOVERY = "vesync_discovery_{}" SERVICE_UPDATE_DEVS = "update_devices" VS_SWITCHES = "switches" +VS_FANS = "fans" VS_MANAGER = "manager" diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py new file mode 100644 index 00000000000..7d395d93a74 --- /dev/null +++ b/homeassistant/components/vesync/fan.py @@ -0,0 +1,117 @@ +"""Support for VeSync fans.""" +import logging + +from homeassistant.components.fan import ( + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .common import VeSyncDevice +from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_FANS + +_LOGGER = logging.getLogger(__name__) + +DEV_TYPE_TO_HA = { + "LV-PUR131S": "fan", +} + +SPEED_AUTO = "auto" +FAN_SPEEDS = [SPEED_AUTO, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the VeSync fan platform.""" + + async def async_discover(devices): + """Add new devices to platform.""" + _async_setup_entities(devices, async_add_entities) + + disp = async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_FANS), async_discover) + hass.data[DOMAIN][VS_DISPATCHERS].append(disp) + + _async_setup_entities(hass.data[DOMAIN][VS_FANS], async_add_entities) + return True + + +@callback +def _async_setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + dev_list = [] + for dev in devices: + if DEV_TYPE_TO_HA.get(dev.device_type) == "fan": + dev_list.append(VeSyncFanHA(dev)) + else: + _LOGGER.warning( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + continue + + async_add_entities(dev_list, update_before_add=True) + + +class VeSyncFanHA(VeSyncDevice, FanEntity): + """Representation of a VeSync fan.""" + + def __init__(self, fan): + """Initialize the VeSync fan device.""" + super().__init__(fan) + self.smartfan = fan + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_SET_SPEED + + @property + def speed(self): + """Return the current speed.""" + if self.smartfan.mode == SPEED_AUTO: + return SPEED_AUTO + if self.smartfan.mode == "manual": + current_level = self.smartfan.fan_level + if current_level is not None: + return FAN_SPEEDS[current_level] + return None + + @property + def speed_list(self): + """Get the list of available speeds.""" + return FAN_SPEEDS + + @property + def unique_info(self): + """Return the ID of this fan.""" + return self.smartfan.uuid + + @property + def device_state_attributes(self): + """Return the state attributes of the fan.""" + return { + "mode": self.smartfan.mode, + "active_time": self.smartfan.active_time, + "filter_life": self.smartfan.filter_life, + "air_quality": self.smartfan.air_quality, + "screen_status": self.smartfan.screen_status, + } + + def set_speed(self, speed): + """Set the speed of the device.""" + if not self.smartfan.is_on: + self.smartfan.turn_on() + + if speed is None or speed == SPEED_AUTO: + self.smartfan.auto_mode() + else: + self.smartfan.manual_mode() + self.smartfan.change_fan_speed(FAN_SPEEDS.index(speed)) + + def turn_on(self, speed: str = None, **kwargs) -> None: + """Turn the device on.""" + self.smartfan.turn_on() + self.set_speed(speed) diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 7ac8e89fb60..a4e786c2a12 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -1,8 +1,14 @@ { "domain": "vesync", - "name": "Etekcity VeSync", + "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", - "codeowners": ["@markperdue", "@webdjoe"], - "requirements": ["pyvesync==1.1.0"], + "codeowners": [ + "@markperdue", + "@webdjoe", + "@thegardenmonkey" + ], + "requirements": [ + "pyvesync==1.1.0" + ], "config_flow": true -} +} \ No newline at end of file diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index fb6e83227e9..939240349d1 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -1,4 +1,4 @@ -"""Support for Etekcity VeSync switches.""" +"""Support for VeSync switches.""" import logging from homeassistant.components.switch import SwitchEntity @@ -55,7 +55,15 @@ def _async_setup_entities(devices, async_add_entities): async_add_entities(dev_list, update_before_add=True) -class VeSyncSwitchHA(VeSyncDevice, SwitchEntity): +class VeSyncBaseSwitch(VeSyncDevice, SwitchEntity): + """Base class for VeSync switch Device Representations.""" + + def turn_on(self, **kwargs): + """Turn the device on.""" + self.device.turn_on() + + +class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): """Representation of a VeSync switch.""" def __init__(self, plug): @@ -90,7 +98,7 @@ class VeSyncSwitchHA(VeSyncDevice, SwitchEntity): self.smartplug.update_energy() -class VeSyncLightSwitch(VeSyncDevice, SwitchEntity): +class VeSyncLightSwitch(VeSyncBaseSwitch, SwitchEntity): """Handle representation of VeSync Light Switch.""" def __init__(self, switch): From ed3e04b9edff9f4619276cc8db8e82d6e36375be Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Sep 2020 08:57:12 +0200 Subject: [PATCH 625/862] Fix Spotify scopes validation for re-auth (#39638) --- homeassistant/components/spotify/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 3f1452742f1..9459a4923c8 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_SPOTIFY_SESSION: session, } - if set(session.token["scope"].split(" ")) <= set(SPOTIFY_SCOPES): + if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, From fdb737d1d956b4bb3c715d7f686333c852e704f4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Sep 2020 09:23:24 +0200 Subject: [PATCH 626/862] Upgrade isort to 5.5.0 (#39639) --- .pre-commit-config.yaml | 4 ++-- homeassistant/components/sense/config_flow.py | 1 - requirements_test_pre_commit.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8af0a7553e8..3f0c4c918ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,8 +38,8 @@ repos: - --format=custom - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - - repo: https://github.com/timothycrosley/isort - rev: 5.4.2 + - repo: https://github.com/PyCQA/isort + rev: 5.5.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 7a5b04229a1..f8b8ede6a4c 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -8,7 +8,6 @@ from homeassistant import config_entries, core from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, SENSE_TIMEOUT_EXCEPTIONS - from .const import DOMAIN # pylint:disable=unused-import; pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 84e0f4e9905..f5a697982bc 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -5,7 +5,7 @@ black==20.8b1 codespell==1.17.1 flake8-docstrings==1.5.0 flake8==3.8.3 -isort==5.4.2 +isort==5.5.0 pydocstyle==5.1.1 pyupgrade==2.7.2 yamllint==1.24.2 From dd7f282723d72163c71a3a2b86c1b762772288e0 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 4 Sep 2020 04:32:36 -0500 Subject: [PATCH 627/862] Add new Plex movie lookup method for media_player.play_media (#39584) --- homeassistant/components/plex/errors.py | 4 + homeassistant/components/plex/media_search.py | 116 ++++++++++++++ homeassistant/components/plex/server.py | 149 ++++-------------- tests/components/plex/mock_classes.py | 8 +- tests/components/plex/test_server.py | 50 +++++- 5 files changed, 202 insertions(+), 125 deletions(-) create mode 100644 homeassistant/components/plex/media_search.py diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py index 534c553d45e..aacc340e2b1 100644 --- a/homeassistant/components/plex/errors.py +++ b/homeassistant/components/plex/errors.py @@ -16,3 +16,7 @@ class ServerNotSpecified(PlexException): class ShouldUpdateConfigEntry(PlexException): """Config entry data is out of date and should be updated.""" + + +class MediaNotFound(PlexException): + """Media lookup failed for a given search query.""" diff --git a/homeassistant/components/plex/media_search.py b/homeassistant/components/plex/media_search.py new file mode 100644 index 00000000000..5992a49bf3b --- /dev/null +++ b/homeassistant/components/plex/media_search.py @@ -0,0 +1,116 @@ +"""Helper methods to search for Plex media.""" +import logging + +from plexapi.exceptions import BadRequest, NotFound + +from .errors import MediaNotFound + +_LOGGER = logging.getLogger(__name__) + + +def lookup_movie(library_section, **kwargs): + """Find a specific movie and return a Plex media object.""" + try: + title = kwargs["title"] + except KeyError: + _LOGGER.error("Must specify 'title' for this search") + return None + + try: + movies = library_section.search(**kwargs, libtype="movie", maxresults=3) + except BadRequest as err: + _LOGGER.error("Invalid search payload provided: %s", err) + return None + + if not movies: + raise MediaNotFound(f"Movie {title}") from None + + if len(movies) > 1: + exact_matches = [x for x in movies if x.title.lower() == title.lower()] + if len(exact_matches) == 1: + return exact_matches[0] + match_list = [f"{x.title} ({x.year})" for x in movies] + _LOGGER.warning("Multiple matches found during search: %s", match_list) + return None + + return movies[0] + + +def lookup_tv(library_section, **kwargs): + """Find TV media and return a Plex media object.""" + season_number = kwargs.get("season_number") + episode_number = kwargs.get("episode_number") + + try: + show_name = kwargs["show_name"] + show = library_section.get(show_name) + except KeyError: + _LOGGER.error("Must specify 'show_name' for this search") + return None + except NotFound as err: + raise MediaNotFound(f"Show {show_name}") from err + + if not season_number: + return show + + try: + season = show.season(int(season_number)) + except NotFound as err: + raise MediaNotFound(f"Season {season_number} of {show_name}") from err + + if not episode_number: + return season + + try: + return season.episode(episode=int(episode_number)) + except NotFound as err: + episode = f"S{str(season_number).zfill(2)}E{str(episode_number).zfill(2)}" + raise MediaNotFound(f"Episode {episode} of {show_name}") from err + + +def lookup_music(library_section, **kwargs): + """Search for music and return a Plex media object.""" + album_name = kwargs.get("album_name") + track_name = kwargs.get("track_name") + track_number = kwargs.get("track_number") + + try: + artist_name = kwargs["artist_name"] + artist = library_section.get(artist_name) + except KeyError: + _LOGGER.error("Must specify 'artist_name' for this search") + return None + except NotFound as err: + raise MediaNotFound(f"Artist {artist_name}") from err + + if album_name: + try: + album = artist.album(album_name) + except NotFound as err: + raise MediaNotFound(f"Album {album_name} by {artist_name}") from err + + if track_name: + try: + return album.track(track_name) + except NotFound as err: + raise MediaNotFound( + f"Track {track_name} on {album_name} by {artist_name}" + ) from err + + if track_number: + for track in album.tracks(): + if int(track.index) == int(track_number): + return track + + raise MediaNotFound( + f"Track {track_number} on {album_name} by {artist_name}" + ) from None + return album + + if track_name: + try: + return artist.get(track_name) + except NotFound as err: + raise MediaNotFound(f"Track {track_name} by {artist_name}") from err + + return artist diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 1d237dedb01..f8706eadf22 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -14,6 +14,7 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player.const import ( MEDIA_TYPE_EPISODE, + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_VIDEO, @@ -41,7 +42,13 @@ from .const import ( X_PLEX_PRODUCT, X_PLEX_VERSION, ) -from .errors import NoServersFound, ServerNotSpecified, ShouldUpdateConfigEntry +from .errors import ( + MediaNotFound, + NoServersFound, + ServerNotSpecified, + ShouldUpdateConfigEntry, +) +from .media_search import lookup_movie, lookup_music, lookup_tv _LOGGER = logging.getLogger(__name__) @@ -487,7 +494,7 @@ class PlexServer: return None try: - library_name = kwargs["library_name"] + library_name = kwargs.pop("library_name") library_section = self.library.section(library_name) except KeyError: _LOGGER.error("Must specify 'library_name' for this search") @@ -496,125 +503,23 @@ class PlexServer: _LOGGER.error("Library '%s' not found", library_name) return None - def lookup_music(): - """Search for music and return a Plex media object.""" - album_name = kwargs.get("album_name") - track_name = kwargs.get("track_name") - track_number = kwargs.get("track_number") - - try: - artist_name = kwargs["artist_name"] - artist = library_section.get(artist_name) - except KeyError: - _LOGGER.error("Must specify 'artist_name' for this search") - return None - except NotFound: - _LOGGER.error( - "Artist '%s' not found in '%s'", artist_name, library_name - ) - return None - - if album_name: + try: + if media_type == MEDIA_TYPE_EPISODE: + return lookup_tv(library_section, **kwargs) + if media_type == MEDIA_TYPE_MOVIE: + return lookup_movie(library_section, **kwargs) + if media_type == MEDIA_TYPE_MUSIC: + return lookup_music(library_section, **kwargs) + if media_type == MEDIA_TYPE_VIDEO: + # Legacy method for compatibility try: - album = artist.album(album_name) - except NotFound: - _LOGGER.error( - "Album '%s' by '%s' not found", album_name, artist_name - ) + video_name = kwargs["video_name"] + return library_section.get(video_name) + except KeyError: + _LOGGER.error("Must specify 'video_name' for this search") return None - - if track_name: - try: - return album.track(track_name) - except NotFound: - _LOGGER.error( - "Track '%s' on '%s' by '%s' not found", - track_name, - album_name, - artist_name, - ) - return None - - if track_number: - for track in album.tracks(): - if int(track.index) == int(track_number): - return track - - _LOGGER.error( - "Track %d on '%s' by '%s' not found", - track_number, - album_name, - artist_name, - ) - return None - return album - - if track_name: - try: - return artist.get(track_name) - except NotFound: - _LOGGER.error( - "Track '%s' by '%s' not found", track_name, artist_name - ) - return None - - return artist - - def lookup_tv(): - """Find TV media and return a Plex media object.""" - season_number = kwargs.get("season_number") - episode_number = kwargs.get("episode_number") - - try: - show_name = kwargs["show_name"] - show = library_section.get(show_name) - except KeyError: - _LOGGER.error("Must specify 'show_name' for this search") - return None - except NotFound: - _LOGGER.error("Show '%s' not found in '%s'", show_name, library_name) - return None - - if not season_number: - return show - - try: - season = show.season(int(season_number)) - except NotFound: - _LOGGER.error( - "Season %d of '%s' not found", - season_number, - show_name, - ) - return None - - if not episode_number: - return season - - try: - return season.episode(episode=int(episode_number)) - except NotFound: - _LOGGER.error( - "Episode not found: %s - S%sE%s", - show_name, - str(season_number).zfill(2), - str(episode_number).zfill(2), - ) - return None - - if media_type == MEDIA_TYPE_MUSIC: - return lookup_music() - if media_type == MEDIA_TYPE_EPISODE: - return lookup_tv() - if media_type == MEDIA_TYPE_VIDEO: - try: - video_name = kwargs["video_name"] - return library_section.get(video_name) - except KeyError: - _LOGGER.error("Must specify 'video_name' for this search") - except NotFound: - _LOGGER.error( - "Movie '%s' not found in '%s'", - video_name, - library_name, - ) + except NotFound as err: + raise MediaNotFound(f"Video {video_name}") from err + except MediaNotFound as failed_item: + _LOGGER.error("%s not found in %s", failed_item, library_name) + return None diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 1fc705be1ca..7cdac1b669a 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -414,6 +414,11 @@ class MockPlexLibrarySection: """Mock the key identifier property.""" return str(id(self.title)) + def search(self, **kwargs): + """Mock the LibrarySection search method.""" + if kwargs.get("libtype") == "movie": + return self.all() + def update(self): """Mock the update call.""" pass @@ -422,11 +427,12 @@ class MockPlexLibrarySection: class MockPlexMediaItem: """Mock a Plex Media instance.""" - def __init__(self, title, mediatype="video"): + def __init__(self, title, mediatype="video", year=2020): """Initialize the object.""" self.title = str(title) self.type = mediatype self.thumbUrl = "http://1.2.3.4/thumb.png" + self.year = year self._children = [] def __iter__(self): diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index d911b258635..b3623681f8a 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,7 +1,7 @@ """Tests for Plex server.""" import copy -from plexapi.exceptions import NotFound +from plexapi.exceptions import BadRequest, NotFound from requests.exceptions import RequestException from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -9,6 +9,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, MEDIA_TYPE_EPISODE, + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_VIDEO, @@ -32,6 +33,7 @@ from .mock_classes import ( MockPlexArtist, MockPlexLibrary, MockPlexLibrarySection, + MockPlexMediaItem, MockPlexSeason, MockPlexServer, MockPlexShow, @@ -454,7 +456,7 @@ async def test_media_lookups(hass): is None ) - # Movie searches + # Legacy Movie searches assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, video_name="Movie") is None assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, library_name="Movies") is None assert loaded_server.lookup_media( @@ -467,3 +469,47 @@ async def test_media_lookups(hass): ) is None ) + + # Movie searches + assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, title="Movie") is None + assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, library_name="Movies") is None + assert loaded_server.lookup_media( + MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie" + ) + with patch.object(MockPlexLibrarySection, "search", side_effect=BadRequest): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie" + ) + is None + ) + with patch.object(MockPlexLibrarySection, "search", return_value=[]): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie" + ) + is None + ) + + similar_movies = [] + for title in "Duplicate Movie", "Duplicate Movie 2": + similar_movies.append(MockPlexMediaItem(title)) + with patch.object( + loaded_server.library.section("Movies"), "search", return_value=similar_movies + ): + found_media = loaded_server.lookup_media( + MEDIA_TYPE_MOVIE, library_name="Movies", title="Duplicate Movie" + ) + assert found_media.title == "Duplicate Movie" + + duplicate_movies = [] + for title in "Duplicate Movie - Original", "Duplicate Movie - Remake": + duplicate_movies.append(MockPlexMediaItem(title)) + with patch.object( + loaded_server.library.section("Movies"), "search", return_value=duplicate_movies + ): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MOVIE, library_name="Movies", title="Duplicate Movie" + ) + ) is None From 8fd9f56d210bb99d20f0984db46cf3f5a072c6af Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 4 Sep 2020 11:33:31 +0200 Subject: [PATCH 628/862] Upgrade wled to 0.4.4 (#39641) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index e7cc99813cb..a646c41d832 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.4.3"], + "requirements": ["wled==0.4.4"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index 495e4b9cdd3..aa9d2e203ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2242,7 +2242,7 @@ wirelesstagpy==0.4.1 withings-api==2.1.6 # homeassistant.components.wled -wled==0.4.3 +wled==0.4.4 # homeassistant.components.wolflink wolf_smartset==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa07cfea08d..4a646758c9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1033,7 +1033,7 @@ wiffi==1.0.1 withings-api==2.1.6 # homeassistant.components.wled -wled==0.4.3 +wled==0.4.4 # homeassistant.components.wolflink wolf_smartset==0.1.4 From ad3e520e09bf22ba0fbe368e036fd5a6750ef4b5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 4 Sep 2020 11:44:22 +0200 Subject: [PATCH 629/862] Fix sensors without unit attribute in Shelly integration (#39629) --- homeassistant/components/shelly/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index cfeaea98357..d66076d1754 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -72,8 +72,6 @@ class ShellySensor(ShellyBlockEntity, Entity): unit = TEMP_CELSIUS else: unit = TEMP_FAHRENHEIT - elif self.info[aioshelly.BLOCK_VALUE_TYPE] == aioshelly.BLOCK_VALUE_TYPE_ENERGY: - unit = ENERGY_KILO_WATT_HOUR self._unit = unit self._device_class = device_class @@ -104,9 +102,9 @@ class ShellySensor(ShellyBlockEntity, Entity): ]: return round(value, 1) # Energy unit change from Wmin or Wh to kWh - if self.info[aioshelly.BLOCK_VALUE_UNIT] == "Wmin": + if self.info.get(aioshelly.BLOCK_VALUE_UNIT) == "Wmin": return round(value / 60 / 1000, 2) - if self.info[aioshelly.BLOCK_VALUE_UNIT] == "Wh": + if self.info.get(aioshelly.BLOCK_VALUE_UNIT) == "Wh": return round(value / 1000, 2) return value From 60c7a917948d7d6cc53ad624c9e465f6f751c225 Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Fri, 4 Sep 2020 12:39:26 +0200 Subject: [PATCH 630/862] Add payload to debug output of rest_command (#39190) --- homeassistant/components/rest_command/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 1290912897d..43b5c8bd7e8 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -122,15 +122,17 @@ async def async_setup(hass, config): if response.status < HTTP_BAD_REQUEST: _LOGGER.debug( - "Success. Url: %s. Status code: %d", + "Success. Url: %s. Status code: %d. Payload: %s", response.url, response.status, + payload, ) else: _LOGGER.warning( - "Error. Url: %s. Status code %d", + "Error. Url: %s. Status code %d. Payload: %s", response.url, response.status, + payload, ) except asyncio.TimeoutError: From 08d93b834989def44461fc6e7dc57560e445ac42 Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Fri, 4 Sep 2020 03:48:15 -0700 Subject: [PATCH 631/862] Make resilient to errors while receiving SMS (#37577) --- homeassistant/components/sms/gateway.py | 5 ++++- homeassistant/components/sms/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 000434561bc..2d36444c03d 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -108,7 +108,10 @@ class Gateway: # delete retrieved sms _LOGGER.debug("Deleting message") - state_machine.DeleteSMS(Folder=0, Location=entry[0]["Location"]) + try: + state_machine.DeleteSMS(Folder=0, Location=entry[0]["Location"]) + except gammu.ERR_MEMORY_NOT_AVAILABLE: + _LOGGER.error("Error deleting SMS, memory not available") else: _LOGGER.debug("Not all parts have arrived") break diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json index c3c7db2aa61..1c24777bd4a 100644 --- a/homeassistant/components/sms/manifest.json +++ b/homeassistant/components/sms/manifest.json @@ -3,6 +3,6 @@ "name": "SMS notifications via GSM-modem", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sms", - "requirements": ["python-gammu==3.0"], + "requirements": ["python-gammu==3.1"], "codeowners": ["@ocalvo"] } diff --git a/requirements_all.txt b/requirements_all.txt index aa9d2e203ed..6b6c2fcd070 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1703,7 +1703,7 @@ python-family-hub-local==0.0.2 python-forecastio==1.4.0 # homeassistant.components.sms -# python-gammu==3.0 +# python-gammu==3.1 # homeassistant.components.gc100 python-gc100==1.0.3a From f207d463907af898fb15ce2cfcedbebdf70fecfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Snoen?= Date: Fri, 4 Sep 2020 13:54:20 +0200 Subject: [PATCH 632/862] Allow using environment cacert file (#38816) --- homeassistant/util/ssl.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 719910987e8..7b987d8eeb2 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -1,4 +1,5 @@ """Helper to create SSL contexts.""" +from os import environ import ssl import certifi @@ -6,9 +7,12 @@ import certifi def client_context() -> ssl.SSLContext: """Return an SSL context for making requests.""" - context = ssl.create_default_context( - purpose=ssl.Purpose.SERVER_AUTH, cafile=certifi.where() - ) + + # Reuse environment variable definition from requests, since it's already a requirement + # If the environment variable has no value, fall back to using certs from certifi package + cafile = environ.get("REQUESTS_CA_BUNDLE", certifi.where()) + + context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile) return context From fa1b8c824c34de2830464a40724e4dcefed565b4 Mon Sep 17 00:00:00 2001 From: Markus Bong <2Fake1987@gmail.com> Date: Fri, 4 Sep 2020 15:33:54 +0200 Subject: [PATCH 633/862] Add devolo thermostat devices (#38594) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/devolo_home_control/climate.py | 116 ++++++++++++++++++ .../components/devolo_home_control/const.py | 2 +- 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/devolo_home_control/climate.py diff --git a/.coveragerc b/.coveragerc index 0730843ccb6..2dabb5e966f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -166,6 +166,7 @@ omit = homeassistant/components/deutsche_bahn/sensor.py homeassistant/components/devolo_home_control/__init__.py homeassistant/components/devolo_home_control/binary_sensor.py + homeassistant/components/devolo_home_control/climate.py homeassistant/components/devolo_home_control/const.py homeassistant/components/devolo_home_control/cover.py homeassistant/components/devolo_home_control/devolo_device.py diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py new file mode 100644 index 00000000000..d44a0c981f1 --- /dev/null +++ b/homeassistant/components/devolo_home_control/climate.py @@ -0,0 +1,116 @@ +"""Platform for climate integration.""" +import logging +from typing import List, Optional + +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, + HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, + TEMP_CELSIUS, + ClimateEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PRECISION_HALVES +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DOMAIN +from .devolo_device import DevoloDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Get all cover devices and setup them via config entry.""" + entities = [] + + for device in hass.data[DOMAIN]["homecontrol"].multi_level_switch_devices: + for multi_level_switch in device.multi_level_switch_property: + if device.deviceModelUID in [ + "devolo.model.Thermostat:Valve", + "devolo.model.Room:Thermostat", + ]: + entities.append( + DevoloClimateDeviceEntity( + homecontrol=hass.data[DOMAIN]["homecontrol"], + device_instance=device, + element_uid=multi_level_switch, + ) + ) + + async_add_entities(entities, False) + + +class DevoloClimateDeviceEntity(DevoloDeviceEntity, ClimateEntity): + """Representation of a climate/thermostat device within devolo Home Control.""" + + def __init__(self, homecontrol, device_instance, element_uid): + """Initialize a devolo climate/thermostat device.""" + super().__init__( + homecontrol=homecontrol, + device_instance=device_instance, + element_uid=element_uid, + name=device_instance.item_name, + sync=self._sync, + ) + + self._multi_level_switch_property = ( + device_instance.multi_level_switch_property.get(element_uid) + ) + + self._temperature = self._multi_level_switch_property.value + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self._temperature + + @property + def hvac_mode(self) -> str: + """Return the supported HVAC mode.""" + return HVAC_MODE_HEAT + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return [HVAC_MODE_HEAT] + + @property + def min_temp(self) -> float: + """Return the minimum set temperature value.""" + return self._multi_level_switch_property.min + + @property + def max_temp(self) -> float: + """Return the maximum set temperature value.""" + return self._multi_level_switch_property.max + + @property + def precision(self) -> float: + """Return the precision of the set temperature.""" + return PRECISION_HALVES + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def temperature_unit(self) -> str: + """Return the supported unit of temperature.""" + return TEMP_CELSIUS + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + self._multi_level_switch_property.set(kwargs[ATTR_TEMPERATURE]) + + def _sync(self, message=None): + """Update the climate entity triggered by web socket connection.""" + if message[0] == self._unique_id: + self._temperature = message[1] + elif message[0].startswith("hdm"): + self._available = self._device_instance.is_online() + else: + _LOGGER.debug("Not valid message received: %s", message) + self.schedule_update_ha_state() diff --git a/homeassistant/components/devolo_home_control/const.py b/homeassistant/components/devolo_home_control/const.py index b98346539d0..08f9f99079e 100644 --- a/homeassistant/components/devolo_home_control/const.py +++ b/homeassistant/components/devolo_home_control/const.py @@ -3,6 +3,6 @@ DOMAIN = "devolo_home_control" DEFAULT_MYDEVOLO = "https://www.mydevolo.com" DEFAULT_MPRM = "https://homecontrol.mydevolo.com" -PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "climate", "cover", "light", "sensor", "switch"] CONF_MYDEVOLO = "mydevolo_url" CONF_HOMECONTROL = "home_control_url" From a096c20930586f40462bf968c20f11edb833db2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Sep 2020 09:28:56 -0500 Subject: [PATCH 634/862] Fix missing assert in template test (#39648) --- tests/helpers/test_template.py | 43 +++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 237e529652a..6a1a4e6f58c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1599,26 +1599,31 @@ def test_nested_async_render_to_info_case(hass): def test_result_as_boolean(hass): """Test converting a template result to a boolean.""" - template.result_as_boolean(True) is True - template.result_as_boolean(" 1 ") is True - template.result_as_boolean(" true ") is True - template.result_as_boolean(" TrUE ") is True - template.result_as_boolean(" YeS ") is True - template.result_as_boolean(" On ") is True - template.result_as_boolean(" Enable ") is True - template.result_as_boolean(1) is True - template.result_as_boolean(-1) is True - template.result_as_boolean(500) is True + assert template.result_as_boolean(True) is True + assert template.result_as_boolean(" 1 ") is True + assert template.result_as_boolean(" true ") is True + assert template.result_as_boolean(" TrUE ") is True + assert template.result_as_boolean(" YeS ") is True + assert template.result_as_boolean(" On ") is True + assert template.result_as_boolean(" Enable ") is True + assert template.result_as_boolean(1) is True + assert template.result_as_boolean(-1) is True + assert template.result_as_boolean(500) is True + assert template.result_as_boolean(0.5) is True + assert template.result_as_boolean(0.389) is True + assert template.result_as_boolean(35) is True - template.result_as_boolean(False) is False - template.result_as_boolean(" 0 ") is False - template.result_as_boolean(" false ") is False - template.result_as_boolean(" FaLsE ") is False - template.result_as_boolean(" no ") is False - template.result_as_boolean(" off ") is False - template.result_as_boolean(" disable ") is False - template.result_as_boolean(0) is False - template.result_as_boolean(None) is False + assert template.result_as_boolean(False) is False + assert template.result_as_boolean(" 0 ") is False + assert template.result_as_boolean(" false ") is False + assert template.result_as_boolean(" FaLsE ") is False + assert template.result_as_boolean(" no ") is False + assert template.result_as_boolean(" off ") is False + assert template.result_as_boolean(" disable ") is False + assert template.result_as_boolean(0) is False + assert template.result_as_boolean(0.0) is False + assert template.result_as_boolean("0.00") is False + assert template.result_as_boolean(None) is False def test_closest_function_to_entity_id(hass): From 0b7576b63475de072f3b0dfcb20bf5649cc97938 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 4 Sep 2020 16:42:57 +0200 Subject: [PATCH 635/862] Add some missing sensors for Shelly integration (#39651) --- homeassistant/components/shelly/sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index d66076d1754..f10e0864061 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -4,6 +4,7 @@ import aioshelly from homeassistant.components import sensor from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, + DEGREE, ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, POWER_WATT, @@ -26,8 +27,11 @@ SENSORS = { "energyReturned": [ENERGY_KILO_WATT_HOUR, sensor.DEVICE_CLASS_ENERGY], "extTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], "humidity": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_HUMIDITY], + "luminosity": ["lx", sensor.DEVICE_CLASS_ILLUMINANCE], "overpowerValue": [POWER_WATT, sensor.DEVICE_CLASS_POWER], "power": [POWER_WATT, sensor.DEVICE_CLASS_POWER], + "powerFactor": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_POWER_FACTOR], + "tilt": [DEGREE, None], "voltage": [VOLT, sensor.DEVICE_CLASS_VOLTAGE], } @@ -93,6 +97,8 @@ class ShellySensor(ShellyBlockEntity, Entity): if value is None: return None + if self.attribute in ["luminosity", "tilt"]: + return round(value) if self.attribute in [ "deviceTemp", "extTemp", @@ -101,6 +107,8 @@ class ShellySensor(ShellyBlockEntity, Entity): "power", ]: return round(value, 1) + if self.attribute == "powerFactor": + return round(value * 100, 1) # Energy unit change from Wmin or Wh to kWh if self.info.get(aioshelly.BLOCK_VALUE_UNIT) == "Wmin": return round(value / 60 / 1000, 2) From 55040cfde5a35c17e768310734139a9f79b359a5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 4 Sep 2020 09:47:39 -0500 Subject: [PATCH 636/862] Reduce log level for Plex service call (#39647) --- homeassistant/components/plex/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 857f6fcf54e..c16da485b57 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -28,7 +28,7 @@ async def async_setup_services(hass): await hass.async_add_executor_job(refresh_library, hass, service_call) async def async_scan_clients_service(_): - _LOGGER.info("Scanning for new Plex clients") + _LOGGER.debug("Scanning for new Plex clients") for server_id in hass.data[DOMAIN][SERVERS]: async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) From ebc31c0f087c93cc4fb5ddd29b179c3392622763 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Sep 2020 09:54:27 -0500 Subject: [PATCH 637/862] Do not treat nexia http not found as invalid auth (#39484) Co-authored-by: Paulus Schoutsen --- homeassistant/components/nexia/__init__.py | 13 +--- homeassistant/components/nexia/config_flow.py | 13 +--- homeassistant/components/nexia/util.py | 10 +++ tests/components/nexia/test_config_flow.py | 63 ++++++++++++++++++- tests/components/nexia/test_util.py | 20 ++++++ 5 files changed, 98 insertions(+), 21 deletions(-) create mode 100644 tests/components/nexia/test_util.py diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 25dce69417c..37a141407a4 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -9,18 +9,14 @@ from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_INTERNAL_SERVER_ERROR, -) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR +from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) @@ -81,10 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Unable to connect to Nexia service: %s", ex) raise ConfigEntryNotReady from ex except HTTPError as http_ex: - if ( - http_ex.response.status_code >= HTTP_BAD_REQUEST - and http_ex.response.status_code < HTTP_INTERNAL_SERVER_ERROR - ): + if is_invalid_auth_code(http_ex.response.status_code): _LOGGER.error( "Access error from Nexia service, please check credentials: %s", http_ex ) diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index b5163d88f63..2054a5de421 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -6,14 +6,10 @@ from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_INTERNAL_SERVER_ERROR, -) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DOMAIN # pylint:disable=unused-import +from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) @@ -42,10 +38,7 @@ async def validate_input(hass: core.HomeAssistant, data): raise CannotConnect from ex except HTTPError as http_ex: _LOGGER.error("HTTP error from Nexia service: %s", http_ex) - if ( - http_ex.response.status_code >= HTTP_BAD_REQUEST - and http_ex.response.status_code < HTTP_INTERNAL_SERVER_ERROR - ): + if is_invalid_auth_code(http_ex.response.status_code): raise InvalidAuth from http_ex raise CannotConnect from http_ex diff --git a/homeassistant/components/nexia/util.py b/homeassistant/components/nexia/util.py index d2ff10c8d34..665aa137065 100644 --- a/homeassistant/components/nexia/util.py +++ b/homeassistant/components/nexia/util.py @@ -1,5 +1,15 @@ """Utils for Nexia / Trane XL Thermostats.""" +from homeassistant.const import HTTP_FORBIDDEN, HTTP_UNAUTHORIZED + + +def is_invalid_auth_code(http_status_code): + """HTTP status codes that mean invalid auth.""" + if http_status_code in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): + return True + + return False + def percent_conv(val): """Convert an actual percentage (0.0-1.0) to 0-100 scale.""" diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index 2d9ca00fbe3..944eb612038 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,5 +1,5 @@ """Test the nexia config flow.""" -from requests.exceptions import ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import config_entries, setup from homeassistant.components.nexia.const import DOMAIN @@ -80,6 +80,67 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} +async def test_form_invalid_auth_http_401(hass): + """Test we handle invalid auth error from http 401.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + response_mock = MagicMock() + type(response_mock).status_code = 401 + with patch( + "homeassistant.components.nexia.config_flow.NexiaHome.login", + side_effect=HTTPError(response=response_mock), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect_not_found(hass): + """Test we handle cannot connect from an http not found error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + response_mock = MagicMock() + type(response_mock).status_code = 404 + with patch( + "homeassistant.components.nexia.config_flow.NexiaHome.login", + side_effect=HTTPError(response=response_mock), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_broad_exception(hass): + """Test we handle invalid auth error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nexia.config_flow.NexiaHome.login", + side_effect=ValueError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + async def test_form_import(hass): """Test we get the form with import source.""" await setup.async_setup_component(hass, "persistent_notification", {}) diff --git a/tests/components/nexia/test_util.py b/tests/components/nexia/test_util.py new file mode 100644 index 00000000000..0820c7caf0c --- /dev/null +++ b/tests/components/nexia/test_util.py @@ -0,0 +1,20 @@ +"""The sensor tests for the nexia platform.""" + + +from homeassistant.components.nexia import util +from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED + + +async def test_is_invalid_auth_code(): + """Test for invalid auth.""" + + assert util.is_invalid_auth_code(HTTP_UNAUTHORIZED) is True + assert util.is_invalid_auth_code(HTTP_FORBIDDEN) is True + assert util.is_invalid_auth_code(HTTP_NOT_FOUND) is False + + +async def test_percent_conv(): + """Test percentage conversion.""" + + assert util.percent_conv(0.12) == 12.0 + assert util.percent_conv(0.123) == 12.3 From f01a0f9151e6e382984f4b3018843532eaf2b5bb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 4 Sep 2020 09:58:40 -0500 Subject: [PATCH 638/862] Allow separate URL for REST switch state (#39557) --- homeassistant/components/rest/switch.py | 8 ++- tests/components/rest/test_switch.py | 87 +++++++++++++++++++------ 2 files changed, 73 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 6a9b4b2ac5b..865f4d01b3a 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -30,6 +30,7 @@ _LOGGER = logging.getLogger(__name__) CONF_BODY_OFF = "body_off" CONF_BODY_ON = "body_on" CONF_IS_ON_TEMPLATE = "is_on_template" +CONF_STATE_RESOURCE = "state_resource" DEFAULT_METHOD = "post" DEFAULT_BODY_OFF = "OFF" @@ -43,6 +44,7 @@ SUPPORT_REST_METHODS = ["post", "put"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.url, + vol.Optional(CONF_STATE_RESOURCE): cv.url, vol.Optional(CONF_HEADERS): {cv.string: cv.string}, vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template, vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template, @@ -73,6 +75,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) resource = config.get(CONF_RESOURCE) + state_resource = config.get(CONF_STATE_RESOURCE) or resource verify_ssl = config.get(CONF_VERIFY_SSL) auth = None @@ -91,6 +94,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= switch = RestSwitch( name, resource, + state_resource, method, headers, auth, @@ -122,6 +126,7 @@ class RestSwitch(SwitchEntity): self, name, resource, + state_resource, method, headers, auth, @@ -135,6 +140,7 @@ class RestSwitch(SwitchEntity): self._state = None self._name = name self._resource = resource + self._state_resource = state_resource self._method = method self._headers = headers self._auth = auth @@ -213,7 +219,7 @@ class RestSwitch(SwitchEntity): with async_timeout.timeout(self._timeout): req = await websession.get( - self._resource, auth=self._auth, headers=self._headers + self._state_resource, auth=self._auth, headers=self._headers ) text = await req.text() diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index bba6ac3dc89..065645fffd1 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -4,7 +4,17 @@ import asyncio import aiohttp import homeassistant.components.rest.switch as rest -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + CONF_HEADERS, + CONF_NAME, + CONF_PLATFORM, + CONF_RESOURCE, + CONTENT_TYPE_JSON, + HTTP_INTERNAL_SERVER_ERROR, + HTTP_NOT_FOUND, + HTTP_OK, +) from homeassistant.helpers.template import Template from homeassistant.setup import setup_component @@ -25,7 +35,7 @@ class TestRestSwitchSetup: def test_setup_missing_config(self): """Test setup with configuration missing required entries.""" assert not asyncio.run_coroutine_threadsafe( - rest.async_setup_platform(self.hass, {"platform": "rest"}, None), + rest.async_setup_platform(self.hass, {CONF_PLATFORM: rest.DOMAIN}, None), self.hass.loop, ).result() @@ -33,7 +43,9 @@ class TestRestSwitchSetup: """Test setup with resource missing schema.""" assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform( - self.hass, {"platform": "rest", "resource": "localhost"}, None + self.hass, + {CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "localhost"}, + None, ), self.hass.loop, ).result() @@ -43,7 +55,9 @@ class TestRestSwitchSetup: aioclient_mock.get("http://localhost", exc=aiohttp.ClientError) assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform( - self.hass, {"platform": "rest", "resource": "http://localhost"}, None + self.hass, + {CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"}, + None, ), self.hass.loop, ).result() @@ -53,41 +67,70 @@ class TestRestSwitchSetup: aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError()) assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform( - self.hass, {"platform": "rest", "resource": "http://localhost"}, None + self.hass, + {CONF_PLATFORM: rest.DOMAIN, CONF_RESOURCE: "http://localhost"}, + None, ), self.hass.loop, ).result() def test_setup_minimum(self, aioclient_mock): """Test setup with minimum configuration.""" - aioclient_mock.get("http://localhost", status=200) - with assert_setup_component(1, "switch"): + aioclient_mock.get("http://localhost", status=HTTP_OK) + with assert_setup_component(1, SWITCH_DOMAIN): assert setup_component( self.hass, - "switch", - {"switch": {"platform": "rest", "resource": "http://localhost"}}, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + CONF_PLATFORM: rest.DOMAIN, + CONF_RESOURCE: "http://localhost", + } + }, ) assert aioclient_mock.call_count == 1 def test_setup(self, aioclient_mock): """Test setup with valid configuration.""" - aioclient_mock.get("http://localhost", status=200) + aioclient_mock.get("http://localhost", status=HTTP_OK) assert setup_component( self.hass, - "switch", + SWITCH_DOMAIN, { - "switch": { - "platform": "rest", - "name": "foo", - "resource": "http://localhost", - "headers": {"Content-type": "application/json"}, - "body_on": "custom on text", - "body_off": "custom off text", + SWITCH_DOMAIN: { + CONF_PLATFORM: rest.DOMAIN, + CONF_NAME: "foo", + CONF_RESOURCE: "http://localhost", + CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, + rest.CONF_BODY_ON: "custom on text", + rest.CONF_BODY_OFF: "custom off text", } }, ) assert aioclient_mock.call_count == 1 - assert_setup_component(1, "switch") + assert_setup_component(1, SWITCH_DOMAIN) + + def test_setup_with_state_resource(self, aioclient_mock): + """Test setup with valid configuration.""" + aioclient_mock.get("http://localhost", status=HTTP_NOT_FOUND) + aioclient_mock.get("http://localhost/state", status=HTTP_OK) + assert setup_component( + self.hass, + SWITCH_DOMAIN, + { + SWITCH_DOMAIN: { + CONF_PLATFORM: rest.DOMAIN, + CONF_NAME: "foo", + CONF_RESOURCE: "http://localhost", + rest.CONF_STATE_RESOURCE: "http://localhost/state", + CONF_HEADERS: {"Content-type": "application/json"}, + rest.CONF_BODY_ON: "custom on text", + rest.CONF_BODY_OFF: "custom off text", + } + }, + ) + assert aioclient_mock.call_count == 1 + assert_setup_component(1, SWITCH_DOMAIN) class TestRestSwitch: @@ -99,6 +142,7 @@ class TestRestSwitch: self.name = "foo" self.method = "post" self.resource = "http://localhost/" + self.state_resource = self.resource self.headers = {"Content-type": "application/json"} self.auth = None self.body_on = Template("on", self.hass) @@ -106,6 +150,7 @@ class TestRestSwitch: self.switch = rest.RestSwitch( self.name, self.resource, + self.state_resource, self.method, self.headers, self.auth, @@ -131,7 +176,7 @@ class TestRestSwitch: def test_turn_on_success(self, aioclient_mock): """Test turn_on.""" - aioclient_mock.post(self.resource, status=200) + aioclient_mock.post(self.resource, status=HTTP_OK) asyncio.run_coroutine_threadsafe( self.switch.async_turn_on(), self.hass.loop ).result() @@ -160,7 +205,7 @@ class TestRestSwitch: def test_turn_off_success(self, aioclient_mock): """Test turn_off.""" - aioclient_mock.post(self.resource, status=200) + aioclient_mock.post(self.resource, status=HTTP_OK) asyncio.run_coroutine_threadsafe( self.switch.async_turn_off(), self.hass.loop ).result() From f2b3e63ff655ab2355e9cf461b22bea38cc10e54 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 4 Sep 2020 11:16:29 -0400 Subject: [PATCH 639/862] Media Source implementation for Chromecast (#39305) * Implement local media finder and integrate into cast * update to media source as a platform * Tweak media source design * fix websocket and local source * fix websocket schema * fix playing media * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Add resolve_media websocket * Register that shit * Square brackets * Sign path * add support for multiple media sources and address PR review * fix lint * fix tests from auto whitelisting config/media * allow specifying a name on the media source * add tests * fix for python 3.7 * Apply suggestions from code review Co-authored-by: Paulus Schoutsen * add http back to cast and remove guess_type from executor as there is no i/o Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 1 + homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 48 +++++- .../components/default_config/manifest.json | 1 + .../components/media_source/__init__.py | 124 ++++++++++++++ .../components/media_source/const.py | 7 + .../components/media_source/error.py | 10 ++ .../components/media_source/local_source.py | 160 ++++++++++++++++++ .../components/media_source/manifest.json | 7 + .../components/media_source/models.py | 124 ++++++++++++++ homeassistant/config.py | 2 +- tests/components/media_source/__init__.py | 1 + tests/components/media_source/test_init.py | 158 +++++++++++++++++ .../media_source/test_local_source.py | 66 ++++++++ tests/components/media_source/test_models.py | 27 +++ tests/test_config.py | 6 +- tests/testing_config/media/not_media.txt | 0 tests/testing_config/media/test.mp3 | 0 18 files changed, 738 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/media_source/__init__.py create mode 100644 homeassistant/components/media_source/const.py create mode 100644 homeassistant/components/media_source/error.py create mode 100644 homeassistant/components/media_source/local_source.py create mode 100644 homeassistant/components/media_source/manifest.json create mode 100644 homeassistant/components/media_source/models.py create mode 100644 tests/components/media_source/__init__.py create mode 100644 tests/components/media_source/test_init.py create mode 100644 tests/components/media_source/test_local_source.py create mode 100644 tests/components/media_source/test_models.py create mode 100644 tests/testing_config/media/not_media.txt create mode 100644 tests/testing_config/media/test.mp3 diff --git a/CODEOWNERS b/CODEOWNERS index a093bc722be..0caa4be8671 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -244,6 +244,7 @@ homeassistant/components/lutron_caseta/* @swails homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mcp23017/* @jardiamj +homeassistant/components/media_source/* @hunterjm homeassistant/components/mediaroom/* @dgomes homeassistant/components/melcloud/* @vilppuvuorinen homeassistant/components/melissa/* @kennedyshead diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index d172a5d0663..49d26431f5b 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==7.2.1"], - "after_dependencies": ["cloud","tts","zeroconf"], + "after_dependencies": ["cloud", "http", "media_source", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] } diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index c4588b3c4c3..342d6f1bee5 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1,5 +1,7 @@ """Provide functionality to interact with Cast devices on the network.""" import asyncio +from datetime import timedelta +import functools as ft import json import logging from typing import Optional @@ -14,12 +16,15 @@ from pychromecast.socket_client import ( ) import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.auth.models import RefreshToken +from homeassistant.components import media_source, zeroconf +from homeassistant.components.http.auth import async_sign_path from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -502,6 +507,44 @@ class CastDevice(MediaPlayerEntity): media_controller = self._media_controller() media_controller.seek(position) + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + result = await media_source.async_browse_media(self.hass, media_content_id) + return result.to_media_player_item() + + async def async_play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" + # Handle media_source + if media_source.is_media_source_id(media_id): + sourced_media = await media_source.async_resolve_media(self.hass, media_id) + media_type = sourced_media.mime_type + media_id = sourced_media.url + + # If media ID is a relative URL, we serve it from HA. + # Create a signed path. + if media_id[0] == "/": + # Sign URL with Home Assistant Cast User + config_entries = self.hass.config_entries.async_entries(CAST_DOMAIN) + user_id = config_entries[0].data["user_id"] + user = await self.hass.auth.async_get_user(user_id) + if user.refresh_tokens: + refresh_token: RefreshToken = list(user.refresh_tokens.values())[0] + + media_id = async_sign_path( + self.hass, + refresh_token.id, + media_id, + timedelta(minutes=5), + ) + + # prepend external URL + hass_url = get_url(self.hass, prefer_external=True) + media_id = f"{hass_url}{media_id}" + + await self.hass.async_add_job( + ft.partial(self.play_media, media_type, media_id, **kwargs) + ) + def play_media(self, media_type, media_id, **kwargs): """Play media from a URL.""" # We do not want this to be forwarded to a group @@ -726,6 +769,9 @@ class CastDevice(MediaPlayerEntity): if media_status.supports_seek: support |= SUPPORT_SEEK + if "media_source" in self.hass.config.components: + support |= SUPPORT_BROWSE_MEDIA + return support @property diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 78da3e1ff50..c25b9b82c38 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -9,6 +9,7 @@ "history", "logbook", "map", + "media_source", "mobile_app", "person", "scene", diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py new file mode 100644 index 00000000000..763baa99b41 --- /dev/null +++ b/homeassistant/components/media_source/__init__.py @@ -0,0 +1,124 @@ +"""The media_source integration.""" +from datetime import timedelta +from typing import Optional + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.http.auth import async_sign_path +from homeassistant.components.media_player.const import ATTR_MEDIA_CONTENT_ID +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) +from homeassistant.loader import bind_hass + +from . import local_source, models +from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX +from .error import Unresolvable + + +def is_media_source_id(media_content_id: str): + """Test if identifier is a media source.""" + return URI_SCHEME_REGEX.match(media_content_id) is not None + + +def generate_media_source_id(domain: str, identifier: str) -> str: + """Generate a media source ID.""" + uri = f"{URI_SCHEME}{domain or ''}" + if identifier: + uri += f"/{identifier}" + return uri + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the media_source component.""" + hass.data[DOMAIN] = {} + hass.components.websocket_api.async_register_command(websocket_browse_media) + hass.components.websocket_api.async_register_command(websocket_resolve_media) + local_source.async_setup(hass) + await async_process_integration_platforms( + hass, DOMAIN, _process_media_source_platform + ) + return True + + +async def _process_media_source_platform(hass, domain, platform): + """Process a media source platform.""" + hass.data[DOMAIN][domain] = await platform.async_get_media_source(hass) + + +@callback +def _get_media_item( + hass: HomeAssistant, media_content_id: Optional[str] +) -> models.MediaSourceItem: + """Return media item.""" + if media_content_id: + return models.MediaSourceItem.from_uri(hass, media_content_id) + + # We default to our own domain if its only one registered + domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN + return models.MediaSourceItem(hass, domain, "") + + +@bind_hass +async def async_browse_media( + hass: HomeAssistant, media_content_id: str +) -> models.BrowseMedia: + """Return media player browse media results.""" + return await _get_media_item(hass, media_content_id).async_browse() + + +@bind_hass +async def async_resolve_media( + hass: HomeAssistant, media_content_id: str +) -> models.PlayMedia: + """Get info to play media.""" + return await _get_media_item(hass, media_content_id).async_resolve() + + +@websocket_api.websocket_command( + { + vol.Required("type"): "media_source/browse_media", + vol.Optional(ATTR_MEDIA_CONTENT_ID, default=""): str, + } +) +@websocket_api.async_response +async def websocket_browse_media(hass, connection, msg): + """Browse available media.""" + try: + media = await async_browse_media(hass, msg.get("media_content_id")) + connection.send_result( + msg["id"], + media.to_media_player_item(), + ) + except BrowseError as err: + connection.send_error(msg["id"], "browse_media_failed", str(err)) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "media_source/resolve_media", + vol.Required(ATTR_MEDIA_CONTENT_ID): str, + vol.Optional("expires", default=30): int, + } +) +@websocket_api.async_response +async def websocket_resolve_media(hass, connection, msg): + """Resolve media.""" + try: + media = await async_resolve_media(hass, msg["media_content_id"]) + url = media.url + except Unresolvable as err: + connection.send_error(msg["id"], "resolve_media_failed", str(err)) + else: + if url[0] == "/": + url = async_sign_path( + hass, + connection.refresh_token_id, + url, + timedelta(seconds=msg["expires"]), + ) + + connection.send_result(msg["id"], {"url": url, "mime_type": media.mime_type}) diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py new file mode 100644 index 00000000000..d50a8b1c404 --- /dev/null +++ b/homeassistant/components/media_source/const.py @@ -0,0 +1,7 @@ +"""Constants for the media_source integration.""" +import re + +DOMAIN = "media_source" +MEDIA_MIME_TYPES = ("audio", "video", "image") +URI_SCHEME = "media-source://" +URI_SCHEME_REGEX = re.compile(r"^media-source://(?P[^/]+)?(?P.+)?") diff --git a/homeassistant/components/media_source/error.py b/homeassistant/components/media_source/error.py new file mode 100644 index 00000000000..00f3ced5d8d --- /dev/null +++ b/homeassistant/components/media_source/error.py @@ -0,0 +1,10 @@ +"""Errors for media source.""" +from homeassistant.exceptions import HomeAssistantError + + +class MediaSourceError(HomeAssistantError): + """Base class for media source errors.""" + + +class Unresolvable(MediaSourceError): + """When media ID is not resolvable.""" diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py new file mode 100644 index 00000000000..2374edf0c33 --- /dev/null +++ b/homeassistant/components/media_source/local_source.py @@ -0,0 +1,160 @@ +"""Local Media Source Implementation.""" +import mimetypes +from pathlib import Path +from typing import Tuple + +from aiohttp import web + +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import sanitize_path + +from .const import DOMAIN, MEDIA_MIME_TYPES +from .models import BrowseMedia, MediaSource, MediaSourceItem, PlayMedia + + +@callback +def async_setup(hass: HomeAssistant): + """Set up local media source.""" + source = LocalSource(hass) + hass.data[DOMAIN][DOMAIN] = source + hass.http.register_view(LocalMediaView(hass)) + + +@callback +def async_parse_identifier(item: MediaSourceItem) -> Tuple[str, str]: + """Parse identifier.""" + if not item.identifier: + source_dir_id = "media" + location = "" + + else: + source_dir_id, location = item.identifier.lstrip("/").split("/", 1) + + if source_dir_id != "media": + raise Unresolvable("Unknown source directory.") + + if location != sanitize_path(location): + raise Unresolvable("Invalid path.") + + return source_dir_id, location + + +class LocalSource(MediaSource): + """Provide local directories as media sources.""" + + name: str = "Local Media" + + def __init__(self, hass: HomeAssistant): + """Initialize local source.""" + super().__init__(DOMAIN) + self.hass = hass + + @callback + def async_full_path(self, source_dir_id, location) -> Path: + """Return full path.""" + return self.hass.config.path("media", location) + + async def async_resolve_media(self, item: MediaSourceItem) -> str: + """Resolve media to a url.""" + source_dir_id, location = async_parse_identifier(item) + mime_type, _ = mimetypes.guess_type( + self.async_full_path(source_dir_id, location) + ) + return PlayMedia(item.identifier, mime_type) + + async def async_browse_media( + self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES + ) -> BrowseMedia: + """Return media.""" + try: + source_dir_id, location = async_parse_identifier(item) + except Unresolvable as err: + raise BrowseError(str(err)) from err + + return await self.hass.async_add_executor_job( + self._browse_media, source_dir_id, location + ) + + def _browse_media(self, source_dir_id, location): + """Browse media.""" + full_path = Path(self.hass.config.path("media", location)) + + if not full_path.exists(): + raise BrowseError("Path does not exist.") + + if not full_path.is_dir(): + raise BrowseError("Path is not a directory.") + + return self._build_item_response(source_dir_id, full_path) + + def _build_item_response(self, source_dir_id: str, path: Path, is_child=False): + mime_type, _ = mimetypes.guess_type(str(path)) + media = BrowseMedia( + DOMAIN, + f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", + path.name, + path.is_file(), + path.is_dir(), + mime_type, + ) + + # Make sure it's a file or directory + if not media.can_play and not media.can_expand: + return None + + # Check that it's a media file + if media.can_play and ( + not mime_type or mime_type.split("/")[0] not in MEDIA_MIME_TYPES + ): + return None + + if not media.can_expand: + return media + + media.name += "/" + + # Append first level children + if not is_child: + media.children = [] + for child_path in path.iterdir(): + child = self._build_item_response(source_dir_id, child_path, True) + if child: + media.children.append(child) + + return media + + +class LocalMediaView(HomeAssistantView): + """ + Local Media Finder View. + + Returns media files in config/media. + """ + + url = "/media/{location:.*}" + name = "media" + + def __init__(self, hass: HomeAssistant): + """Initialize the media view.""" + self.hass = hass + + async def get(self, request: web.Request, location: str) -> web.FileResponse: + """Start a GET request.""" + if location != sanitize_path(location): + return web.HTTPNotFound() + + media_path = Path(self.hass.config.path("media", location)) + + # Check that the file exists + if not media_path.is_file(): + raise web.HTTPNotFound() + + # Check that it's a media file + mime_type, _ = mimetypes.guess_type(str(media_path)) + if not mime_type or mime_type.split("/")[0] not in MEDIA_MIME_TYPES: + raise web.HTTPNotFound() + + return web.FileResponse(media_path) diff --git a/homeassistant/components/media_source/manifest.json b/homeassistant/components/media_source/manifest.json new file mode 100644 index 00000000000..d941c85aced --- /dev/null +++ b/homeassistant/components/media_source/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "media_source", + "name": "Media Source", + "documentation": "https://www.home-assistant.io/integrations/media_source", + "dependencies": ["http"], + "codeowners": ["@hunterjm"] +} diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py new file mode 100644 index 00000000000..02248efa068 --- /dev/null +++ b/homeassistant/components/media_source/models.py @@ -0,0 +1,124 @@ +"""Media Source models.""" +from abc import ABC +from dataclasses import dataclass +from typing import List, Optional, Tuple + +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX + + +@dataclass +class PlayMedia: + """Represents a playable media.""" + + url: str + mime_type: str + + +@dataclass +class BrowseMedia: + """Represent a browsable media file.""" + + domain: str + identifier: str + + name: str + can_play: bool = False + can_expand: bool = False + media_content_type: str = None + children: List = None + + def to_uri(self): + """Return URI of media.""" + uri = f"{URI_SCHEME}{self.domain or ''}" + if self.identifier: + uri += f"/{self.identifier}" + return uri + + def to_media_player_item(self): + """Convert Media class to browse media dictionary.""" + content_type = self.media_content_type + + if content_type is None: + content_type = "folder" if self.can_expand else "file" + + response = { + "title": self.name, + "media_content_type": content_type, + "media_content_id": self.to_uri(), + "can_play": self.can_play, + "can_expand": self.can_expand, + } + + if self.children: + response["children"] = [ + child.to_media_player_item() for child in self.children + ] + + return response + + +@dataclass +class MediaSourceItem: + """A parsed media item.""" + + hass: HomeAssistant + domain: Optional[str] + identifier: str + + async def async_browse(self) -> BrowseMedia: + """Browse this item.""" + if self.domain is None: + base = BrowseMedia(None, None, "Media Sources", False, True) + base.children = [ + BrowseMedia(source.domain, None, source.name, False, True) + for source in self.hass.data[DOMAIN].values() + ] + return base + + return await self.async_media_source().async_browse_media(self) + + async def async_resolve(self) -> PlayMedia: + """Resolve to playable item.""" + return await self.async_media_source().async_resolve_media(self) + + @callback + def async_media_source(self) -> "MediaSource": + """Return media source that owns this item.""" + return self.hass.data[DOMAIN][self.domain] + + @classmethod + def from_uri(cls, hass: HomeAssistant, uri: str) -> "MediaSourceItem": + """Create an item from a uri.""" + match = URI_SCHEME_REGEX.match(uri) + + if not match: + raise ValueError("Invalid media source URI") + + domain = match.group("domain") + identifier = match.group("identifier") + + return cls(hass, domain, identifier) + + +class MediaSource(ABC): + """Represents a source of media files.""" + + name: str = None + + def __init__(self, domain: str): + """Initialize a media source.""" + self.domain = domain + if not self.name: + self.name = domain + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve a media item to a playable item.""" + raise NotImplementedError + + async def async_browse_media( + self, item: MediaSourceItem, media_types: Tuple[str] + ) -> BrowseMedia: + """Browse media.""" + raise NotImplementedError diff --git a/homeassistant/config.py b/homeassistant/config.py index 80b5a203564..36a81f98fa3 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -504,7 +504,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non hac.set_time_zone(config[CONF_TIME_ZONE]) # Init whitelist external dir - hac.allowlist_external_dirs = {hass.config.path("www")} + hac.allowlist_external_dirs = {hass.config.path("www"), hass.config.path("media")} if CONF_ALLOWLIST_EXTERNAL_DIRS in config: hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS])) diff --git a/tests/components/media_source/__init__.py b/tests/components/media_source/__init__.py new file mode 100644 index 00000000000..d5e56d9d31d --- /dev/null +++ b/tests/components/media_source/__init__.py @@ -0,0 +1 @@ +"""The tests for Media Source integration.""" diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py new file mode 100644 index 00000000000..bc1d901e03f --- /dev/null +++ b/tests/components/media_source/test_init.py @@ -0,0 +1,158 @@ +"""Test Media Source initialization.""" +import pytest + +from homeassistant.components import media_source +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source import const +from homeassistant.setup import async_setup_component + +from tests.async_mock import patch + + +async def test_is_media_source_id(): + """Test media source validation.""" + assert media_source.is_media_source_id(const.URI_SCHEME) + assert media_source.is_media_source_id(f"{const.URI_SCHEME}domain") + assert media_source.is_media_source_id(f"{const.URI_SCHEME}domain/identifier") + assert not media_source.is_media_source_id("test") + + +async def test_generate_media_source_id(): + """Test identifier generation.""" + tests = [ + (None, None), + (None, ""), + ("", ""), + ("domain", None), + ("domain", ""), + ("domain", "identifier"), + ] + + for domain, identifier in tests: + assert media_source.is_media_source_id( + media_source.generate_media_source_id(domain, identifier) + ) + + +async def test_async_browse_media(hass): + """Test browse media.""" + assert await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + # Test non-media ignored (/media has test.mp3 and not_media.txt) + media = await media_source.async_browse_media(hass, "") + assert isinstance(media, media_source.models.BrowseMedia) + assert media.name == "media/" + assert len(media.children) == 1 + + # Test invalid media content + with pytest.raises(ValueError): + await media_source.async_browse_media(hass, "invalid") + + # Test base URI returns all domains + media = await media_source.async_browse_media(hass, const.URI_SCHEME) + assert isinstance(media, media_source.models.BrowseMedia) + assert len(media.children) == 1 + assert media.children[0].name == "Local Media" + + +async def test_async_resolve_media(hass): + """Test browse media.""" + assert await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + # Test no media content + media = await media_source.async_resolve_media(hass, "") + assert isinstance(media, media_source.models.PlayMedia) + + +async def test_websocket_browse_media(hass, hass_ws_client): + """Test browse media websocket.""" + assert await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + media = media_source.models.BrowseMedia(const.DOMAIN, "/media", False, True) + + with patch( + "homeassistant.components.media_source.async_browse_media", + return_value=media, + ): + await client.send_json( + { + "id": 1, + "type": "media_source/browse_media", + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert msg["id"] == 1 + assert media.to_media_player_item() == msg["result"] + + with patch( + "homeassistant.components.media_source.async_browse_media", + side_effect=BrowseError("test"), + ): + await client.send_json( + { + "id": 2, + "type": "media_source/browse_media", + "media_content_id": "invalid", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "browse_media_failed" + assert msg["error"]["message"] == "test" + + +async def test_websocket_resolve_media(hass, hass_ws_client): + """Test browse media websocket.""" + assert await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + media = media_source.models.PlayMedia("/media/test.mp3", "audio/mpeg") + + with patch( + "homeassistant.components.media_source.async_resolve_media", + return_value=media, + ): + await client.send_json( + { + "id": 1, + "type": "media_source/resolve_media", + "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3", + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert msg["id"] == 1 + assert msg["result"]["url"].startswith(media.url) + assert msg["result"]["mime_type"] == media.mime_type + + with patch( + "homeassistant.components.media_source.async_resolve_media", + side_effect=media_source.Unresolvable("test"), + ): + await client.send_json( + { + "id": 2, + "type": "media_source/resolve_media", + "media_content_id": "invalid", + } + ) + + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "resolve_media_failed" + assert msg["error"]["message"] == "test" diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py new file mode 100644 index 00000000000..44d38107949 --- /dev/null +++ b/tests/components/media_source/test_local_source.py @@ -0,0 +1,66 @@ +"""Test Local Media Source.""" +import pytest + +from homeassistant.components import media_source +from homeassistant.components.media_source import const +from homeassistant.setup import async_setup_component + + +async def test_async_browse_media(hass): + """Test browse media.""" + assert await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + # Test path not exists + with pytest.raises(media_source.BrowseError) as excinfo: + await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test/not/exist" + ) + assert str(excinfo.value) == "Path does not exist." + + # Test browse file + with pytest.raises(media_source.BrowseError) as excinfo: + await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3" + ) + assert str(excinfo.value) == "Path is not a directory." + + # Test invalid base + with pytest.raises(media_source.BrowseError) as excinfo: + await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{const.DOMAIN}/invalid/base" + ) + assert str(excinfo.value) == "Unknown source directory." + + # Test directory traversal + with pytest.raises(media_source.BrowseError) as excinfo: + await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/../configuration.yaml" + ) + assert str(excinfo.value) == "Invalid path." + + # Test successful listing + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/." + ) + assert media + + +async def test_media_view(hass, hass_client): + """Test media view.""" + assert await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + client = await hass_client() + + # Protects against non-existent files + resp = await client.get("/media/invalid.txt") + assert resp.status == 404 + + # Protects against non-media files + resp = await client.get("/media/not_media.txt") + assert resp.status == 404 + + # Fetch available media + resp = await client.get("/media/test.mp3") + assert resp.status == 200 diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py new file mode 100644 index 00000000000..e7bac5acc9a --- /dev/null +++ b/tests/components/media_source/test_models.py @@ -0,0 +1,27 @@ +"""Test Media Source model methods.""" +from homeassistant.components.media_source import const, models + + +async def test_browse_media_to_media_player_item(): + """Test BrowseMedia conversion to media player item dict.""" + base = models.BrowseMedia(const.DOMAIN, "media", "media/", False, True) + base.children = [ + models.BrowseMedia( + const.DOMAIN, "media/test.mp3", "test.mp3", True, False, "audio/mp3" + ) + ] + + item = base.to_media_player_item() + assert item["title"] == "media/" + assert item["media_content_type"] == "folder" + assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" + assert not item["can_play"] + assert item["can_expand"] + assert len(item["children"]) == 1 + assert item["children"][0]["title"] == "test.mp3" + + +async def test_media_source_default_name(): + """Test MediaSource uses domain as default name.""" + source = models.MediaSource(const.DOMAIN) + assert source.name == const.DOMAIN diff --git a/tests/test_config.py b/tests/test_config.py index 9ec0c166850..c5443666bf5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -364,7 +364,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage): assert hass.config.time_zone.zone == "Europe/Copenhagen" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" - assert len(hass.config.allowlist_external_dirs) == 2 + assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.config_source == SOURCE_STORAGE @@ -421,7 +421,7 @@ async def test_override_stored_configuration(hass, hass_storage): assert hass.config.location_name == "Home" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC assert hass.config.time_zone.zone == "Europe/Copenhagen" - assert len(hass.config.allowlist_external_dirs) == 2 + assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.config_source == config_util.SOURCE_YAML @@ -451,7 +451,7 @@ async def test_loading_configuration(hass): assert hass.config.time_zone.zone == "America/New_York" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" - assert len(hass.config.allowlist_external_dirs) == 2 + assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.config_source == config_util.SOURCE_YAML diff --git a/tests/testing_config/media/not_media.txt b/tests/testing_config/media/not_media.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/testing_config/media/test.mp3 b/tests/testing_config/media/test.mp3 new file mode 100644 index 00000000000..e69de29bb2d From f1ee11cb14d4c7558864d3bd6bec578db58e039b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 4 Sep 2020 18:21:23 +0200 Subject: [PATCH 640/862] Bump brother library (#39657) --- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 2d3163b125a..f107c9573da 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==0.1.15"], + "requirements": ["brother==0.1.17"], "zeroconf": ["_printer._tcp.local."], "config_flow": true, "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index 6b6c2fcd070..f8469be749c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -385,7 +385,7 @@ bravia-tv==1.0.6 broadlink==0.14.1 # homeassistant.components.brother -brother==0.1.15 +brother==0.1.17 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a646758c9c..f8e105bb19f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,7 +204,7 @@ bravia-tv==1.0.6 broadlink==0.14.1 # homeassistant.components.brother -brother==0.1.15 +brother==0.1.17 # homeassistant.components.bsblan bsblan==0.3.7 From 05e15dc3185638a875a3e04e023d06aa4c0fef24 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Fri, 4 Sep 2020 12:23:53 -0400 Subject: [PATCH 641/862] Add unique ID to demo fan (#39658) --- homeassistant/components/demo/fan.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 4b75e817153..ee49f0a2e99 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -18,8 +18,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the demo fan platform.""" async_add_entities( [ - DemoFan(hass, "Living Room Fan", FULL_SUPPORT), - DemoFan(hass, "Ceiling Fan", LIMITED_SUPPORT), + DemoFan(hass, "fan1", "Living Room Fan", FULL_SUPPORT), + DemoFan(hass, "fan2", "Ceiling Fan", LIMITED_SUPPORT), ] ) @@ -32,9 +32,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoFan(FanEntity): """A demonstration fan component.""" - def __init__(self, hass, name: str, supported_features: int) -> None: + def __init__( + self, hass, unique_id: str, name: str, supported_features: int + ) -> None: """Initialize the entity.""" self.hass = hass + self._unique_id = unique_id self._supported_features = supported_features self._speed = STATE_OFF self._oscillating = None @@ -46,6 +49,11 @@ class DemoFan(FanEntity): if supported_features & SUPPORT_DIRECTION: self._direction = "forward" + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def name(self) -> str: """Get entity name.""" From 08d5175d05ce050291d7ebbf81217dc73ee71679 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Fri, 4 Sep 2020 19:19:18 +0100 Subject: [PATCH 642/862] Improve handling of roon radio data (#39659) --- homeassistant/components/roon/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 2f695b49236..a4bd374dcce 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -209,7 +209,7 @@ class RoonDevice(MediaPlayerEntity): media_artist = now_playing_data["three_line"]["line2"] media_album_name = now_playing_data["three_line"]["line3"] media_position = convert(now_playing_data["seek_position"], int, 0) - media_duration = convert(now_playing_data["length"], int, 0) + media_duration = convert(now_playing_data.get("length"), int, 0) image_id = now_playing_data.get("image_key") except KeyError: # catch KeyError From 84944cfc24559c9022a956353a4a464a4f8185ef Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 4 Sep 2020 20:21:42 +0200 Subject: [PATCH 643/862] Add Netatmo media browser support (#39578) --- .../components/media_source/models.py | 2 + homeassistant/components/netatmo/__init__.py | 4 + homeassistant/components/netatmo/camera.py | 28 ++++ homeassistant/components/netatmo/const.py | 2 + .../components/netatmo/manifest.json | 3 +- .../components/netatmo/media_source.py | 141 ++++++++++++++++++ tests/components/netatmo/test_media_source.py | 90 +++++++++++ 7 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/netatmo/media_source.py create mode 100644 tests/components/netatmo/test_media_source.py diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 02248efa068..b93cb961449 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -28,6 +28,7 @@ class BrowseMedia: can_expand: bool = False media_content_type: str = None children: List = None + thumbnail: str = None def to_uri(self): """Return URI of media.""" @@ -49,6 +50,7 @@ class BrowseMedia: "media_content_id": self.to_uri(), "can_play": self.can_play, "can_expand": self.can_expand, + "thumbnail": self.thumbnail, } if self.children: diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index f68683a152d..67e83189fc5 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -26,7 +26,9 @@ from . import api, config_flow from .const import ( AUTH, CONF_CLOUDHOOK_URL, + DATA_CAMERAS, DATA_DEVICE_IDS, + DATA_EVENTS, DATA_HANDLER, DATA_HOMES, DATA_PERSONS, @@ -62,6 +64,8 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.data[DOMAIN][DATA_DEVICE_IDS] = {} hass.data[DOMAIN][DATA_SCHEDULES] = {} hass.data[DOMAIN][DATA_HOMES] = {} + hass.data[DOMAIN][DATA_EVENTS] = {} + hass.data[DOMAIN][DATA_CAMERAS] = {} if DOMAIN not in config: return True diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 210c9d92e3c..3f9720f3adb 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -16,6 +16,8 @@ from .const import ( ATTR_PERSONS, ATTR_PSEUDO, CAMERA_LIGHT_MODES, + DATA_CAMERAS, + DATA_EVENTS, DATA_HANDLER, DATA_PERSONS, DOMAIN, @@ -157,6 +159,8 @@ class NetatmoCamera(NetatmoBase, Camera): ) ) + self.hass.data[DOMAIN][DATA_CAMERAS][self._id] = self._device_name + @callback def handle_event(self, event): """Handle webhook events.""" @@ -275,6 +279,30 @@ class NetatmoCamera(NetatmoBase, Camera): self._is_local = camera.get("is_local") self.is_streaming = bool(self._status == "on") + if self._model == "NACamera": # Smart Indoor Camera + self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( + self._data.events.get(self._id, {}) + ) + elif self._model == "NOC": # Smart Outdoor Camera + self.hass.data[DOMAIN][DATA_EVENTS][ + self._id + ] = self._data.outdoor_events.get(self._id, {}) + + def process_events(self, events): + """Add meta data to events.""" + for event in events.values(): + if "video_id" not in event: + continue + if self._is_local: + event[ + "media_url" + ] = f"{self._localurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" + else: + event[ + "media_url" + ] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" + return events + def _service_set_persons_home(self, **kwargs): """Service to change current home schedule.""" persons = kwargs.get(ATTR_PERSONS) diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 01351723716..138065f086b 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -42,7 +42,9 @@ CONF_UUID = "uuid" OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" +DATA_CAMERAS = "cameras" DATA_DEVICE_IDS = "netatmo_device_ids" +DATA_EVENTS = "netatmo_events" DATA_HOMES = "netatmo_homes" DATA_PERSONS = "netatmo_persons" DATA_SCHEDULES = "netatmo_schedules" diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index fe8c5367093..b7205431bb5 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -6,7 +6,8 @@ "pyatmo==4.0.0" ], "after_dependencies": [ - "cloud" + "cloud", + "media_source" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py new file mode 100644 index 00000000000..a862252f02e --- /dev/null +++ b/homeassistant/components/netatmo/media_source.py @@ -0,0 +1,141 @@ +"""Netatmo Media Source Implementation.""" +import datetime as dt +import re +from typing import Optional, Tuple + +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.const import MEDIA_MIME_TYPES +from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.models import ( + BrowseMedia, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.core import HomeAssistant, callback + +from .const import DATA_CAMERAS, DATA_EVENTS, DOMAIN, MANUFACTURER + +MIME_TYPE = "application/x-mpegURL" + + +async def async_get_media_source(hass: HomeAssistant): + """Set up Netatmo media source.""" + return NetatmoSource(hass) + + +class NetatmoSource(MediaSource): + """Provide Netatmo camera recordings as media sources.""" + + name: str = MANUFACTURER + + def __init__(self, hass: HomeAssistant): + """Initialize Netatmo source.""" + super().__init__(DOMAIN) + self.hass = hass + self.events = self.hass.data[DOMAIN][DATA_EVENTS] + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve media to a url.""" + _, camera_id, event_id = async_parse_identifier(item) + url = self.events[camera_id][event_id]["media_url"] + return PlayMedia(url, MIME_TYPE) + + async def async_browse_media( + self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES + ) -> Optional[BrowseMedia]: + """Return media.""" + try: + source, camera_id, event_id = async_parse_identifier(item) + except Unresolvable as err: + raise BrowseError(str(err)) from err + + return self._browse_media(source, camera_id, event_id) + + def _browse_media( + self, source: str, camera_id: str, event_id: int + ) -> Optional[BrowseMedia]: + """Browse media.""" + if camera_id and camera_id not in self.events: + raise BrowseError("Camera does not exist.") + + if event_id and event_id not in self.events[camera_id]: + raise BrowseError("Event does not exist.") + + return self._build_item_response(source, camera_id, event_id) + + def _build_item_response( + self, source: str, camera_id: str, event_id: int = None + ) -> Optional[BrowseMedia]: + if event_id and event_id in self.events[camera_id]: + created = dt.datetime.fromtimestamp(event_id) + thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url") + message = remove_html_tags(self.events[camera_id][event_id]["message"]) + title = f"{created} - {message}" + else: + title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER) + thumbnail = None + + if event_id: + path = f"{source}/{camera_id}/{event_id}" + else: + path = f"{source}/{camera_id}" + + media = BrowseMedia( + DOMAIN, + path, + title, + ) + + media.can_play = bool( + event_id and self.events[camera_id][event_id].get("media_url") + ) + media.can_expand = event_id is None + media.thumbnail = thumbnail + + if not media.can_play and not media.can_expand: + return None + + if not media.can_expand: + return media + + media.children = [] + # Append first level children + if not camera_id: + for cid in self.events: + child = self._build_item_response(source, cid) + if child: + media.children.append(child) + else: + for eid in self.events[camera_id]: + child = self._build_item_response(source, camera_id, eid) + if child: + media.children.append(child) + + return media + + +def remove_html_tags(text): + """Remove html tags from string.""" + clean = re.compile("<.*?>") + return re.sub(clean, "", text) + + +@callback +def async_parse_identifier( + item: MediaSourceItem, +) -> Tuple[str, str, Optional[int]]: + """Parse identifier.""" + if not item.identifier: + return "events", "", None + + source, path = item.identifier.lstrip("/").split("/", 1) + + if source != "events": + raise Unresolvable("Unknown source directory.") + + if "/" in path: + camera_id, event_id = path.split("/", 1) + return source, camera_id, int(event_id) + + return source, path, None diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py new file mode 100644 index 00000000000..0405317f03e --- /dev/null +++ b/tests/components/netatmo/test_media_source.py @@ -0,0 +1,90 @@ +"""Test Local Media Source.""" +import pytest + +from homeassistant.components import media_source +from homeassistant.components.media_source import const +from homeassistant.components.media_source.models import PlayMedia +from homeassistant.components.netatmo import DATA_CAMERAS, DATA_EVENTS, DOMAIN +from homeassistant.setup import async_setup_component + + +async def test_async_browse_media(hass): + """Test browse media.""" + assert await async_setup_component(hass, DOMAIN, {}) + + # Prepare cached Netatmo event date + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_EVENTS] = { + "12:34:56:78:90:ab": { + 1599152672: { + "id": "12345", + "time": 1599152672, + "camera_id": "12:34:56:78:90:ab", + "snapshot": { + "url": "https://netatmocameraimage", + }, + "video_id": "98765", + "video_status": "available", + "message": "Paulus seen", + "media_url": "http:///files/high/index.m3u8", + }, + 1599152673: { + "id": "12346", + "time": 1599152673, + "camera_id": "12:34:56:78:90:ab", + "snapshot": { + "url": "https://netatmocameraimage", + }, + "message": "Tobias seen", + }, + } + } + hass.data[DOMAIN][DATA_CAMERAS] = {"12:34:56:78:90:ab": "MyCamera"} + + assert await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + # Test camera not exists + with pytest.raises(media_source.BrowseError) as excinfo: + await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/events/98:76:54:32:10:ff" + ) + assert str(excinfo.value) == "Camera does not exist." + + # Test browse event + with pytest.raises(media_source.BrowseError) as excinfo: + await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/12345" + ) + assert str(excinfo.value) == "Event does not exist." + + # Test invalid base + with pytest.raises(media_source.BrowseError) as excinfo: + await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/invalid/base" + ) + assert str(excinfo.value) == "Unknown source directory." + + # Test successful listing + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/events/" + ) + + # Test successful events listing + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab" + ) + + # Test successful event listing + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672" + ) + assert media + + # Test successful event resolve + media = await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672" + ) + assert media == PlayMedia( + url="http:///files/high/index.m3u8", mime_type="application/x-mpegURL" + ) From 767be18265df159f3e838e9c814a437dda2c41ce Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Fri, 4 Sep 2020 20:52:44 +0200 Subject: [PATCH 644/862] Bumping aioasuswrt version so that it have license-tag and manifest (#39654) Took 32 minutes Co-authored-by: magnusknutas --- homeassistant/components/asuswrt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 8c480bcd5fc..97514c4da7f 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -2,6 +2,6 @@ "domain": "asuswrt", "name": "ASUSWRT", "documentation": "https://www.home-assistant.io/integrations/asuswrt", - "requirements": ["aioasuswrt==1.2.7"], + "requirements": ["aioasuswrt==1.2.8"], "codeowners": ["@kennedyshead"] } diff --git a/requirements_all.txt b/requirements_all.txt index f8469be749c..5d5a2ef7c67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -144,7 +144,7 @@ aio_georss_gdacs==0.3 aioambient==1.2.1 # homeassistant.components.asuswrt -aioasuswrt==1.2.7 +aioasuswrt==1.2.8 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8e105bb19f..67251255c40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -72,7 +72,7 @@ aio_georss_gdacs==0.3 aioambient==1.2.1 # homeassistant.components.asuswrt -aioasuswrt==1.2.7 +aioasuswrt==1.2.8 # homeassistant.components.azure_devops aioazuredevops==1.3.5 From 7f7a0003c7d9ef2ca9b35e30fa84c88a2356d089 Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Fri, 4 Sep 2020 20:56:22 +0200 Subject: [PATCH 645/862] Address review comments on dsmr update to ConfigEntry (#39662) --- homeassistant/components/dsmr/config_flow.py | 4 +++- homeassistant/components/dsmr/sensor.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index ecf93177334..d3aa770ff60 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -24,7 +24,7 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): reload_on_update: bool = True, ): """Test if host and port are already configured.""" - for entry in self.hass.config_entries.async_entries(DOMAIN): + for entry in self._async_current_entries(): if entry.data.get(CONF_HOST) == host and entry.data[CONF_PORT] == port: if updates is not None: changed = self.hass.config_entries.async_update_entry( @@ -44,6 +44,8 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="already_configured") + return None + async def async_step_import(self, import_config=None): """Handle the initial step.""" host = import_config.get(CONF_HOST) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 4298afe3cf6..c98dc316d0d 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -200,7 +200,7 @@ async def async_setup_entry( return # Can't be hass.async_add_job because job runs forever - task = hass.loop.create_task(connect_and_reconnect()) + task = asyncio.create_task(connect_and_reconnect()) # Save the task to be able to cancel it when unloading hass.data[DOMAIN][entry.entry_id][DATA_TASK] = task From 9b23d7c2fdca3f8a3a7122e9d63012d02a1afdc7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Sep 2020 14:01:41 -0500 Subject: [PATCH 646/862] Use the shared Zeroconf instance in esphome (#38747) --- homeassistant/components/esphome/__init__.py | 4 ++++ .../components/esphome/config_flow.py | 19 +++++++++++++++++-- .../components/esphome/manifest.json | 7 +++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/test_config_flow.py | 3 ++- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 4248366ffad..f1b22c13bf1 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -17,6 +17,7 @@ from aioesphomeapi import ( import voluptuous as vol from homeassistant import const +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -66,12 +67,15 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] + zeroconf_instance = await zeroconf.async_get_instance(hass) + cli = APIClient( hass.loop, host, port, password, client_info=f"Home Assistant {const.__version__}", + zeroconf_instance=zeroconf_instance, ) # Store client in per-config-entry hass.data diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 5c35909088d..a22256bc69d 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -5,6 +5,7 @@ from typing import Optional from aioesphomeapi import APIClient, APIConnectionError import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback @@ -165,7 +166,14 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def fetch_device_info(self): """Fetch device info from API and return any errors.""" - cli = APIClient(self.hass.loop, self._host, self._port, "") + zeroconf_instance = await zeroconf.async_get_instance(self.hass) + cli = APIClient( + self.hass.loop, + self._host, + self._port, + "", + zeroconf_instance=zeroconf_instance, + ) try: await cli.connect() @@ -181,7 +189,14 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): async def try_login(self): """Try logging in to device and return any errors.""" - cli = APIClient(self.hass.loop, self._host, self._port, self._password) + zeroconf_instance = await zeroconf.async_get_instance(self.hass) + cli = APIClient( + self.hass.loop, + self._host, + self._port, + self._password, + zeroconf_instance=zeroconf_instance, + ) try: await cli.connect(login=True) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 19d00fbbff9..c57ff4a5520 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,10 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==2.6.1"], + "requirements": ["aioesphomeapi==2.6.3"], "zeroconf": ["_esphomelib._tcp.local."], - "codeowners": ["@OttoWinter"] + "codeowners": ["@OttoWinter"], + "after_dependencies": [ + "zeroconf" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index 5d5a2ef7c67..9d64d6d952e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -160,7 +160,7 @@ aiodns==2.0.0 aioeafm==0.1.2 # homeassistant.components.esphome -aioesphomeapi==2.6.1 +aioesphomeapi==2.6.3 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67251255c40..553e84662eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -88,7 +88,7 @@ aiodns==2.0.0 aioeafm==0.1.2 # homeassistant.components.esphome -aioesphomeapi==2.6.1 +aioesphomeapi==2.6.3 # homeassistant.components.flo aioflo==0.4.1 diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 4c5cb15a261..164709e86a8 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -22,11 +22,12 @@ def mock_client(): """Mock APIClient.""" with patch("homeassistant.components.esphome.config_flow.APIClient") as mock_client: - def mock_constructor(loop, host, port, password): + def mock_constructor(loop, host, port, password, zeroconf_instance=None): """Fake the client constructor.""" mock_client.host = host mock_client.port = port mock_client.password = password + mock_client.zeroconf_instance = zeroconf_instance return mock_client mock_client.side_effect = mock_constructor From ad6e8b2d62e7693c920915df0eddea2799ea44d7 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 4 Sep 2020 22:11:07 +0300 Subject: [PATCH 647/862] Add event sensors for risco (#39594) * Add Risco event sensors * Fix lint --- homeassistant/components/risco/__init__.py | 46 +++- homeassistant/components/risco/const.py | 3 + homeassistant/components/risco/manifest.json | 2 +- homeassistant/components/risco/sensor.py | 96 ++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../risco/test_alarm_control_panel.py | 46 +--- tests/components/risco/test_binary_sensor.py | 47 +--- tests/components/risco/test_sensor.py | 207 ++++++++++++++++++ tests/components/risco/util.py | 37 ++++ 10 files changed, 404 insertions(+), 84 deletions(-) create mode 100644 homeassistant/components/risco/sensor.py create mode 100644 tests/components/risco/test_sensor.py create mode 100644 tests/components/risco/util.py diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 09995f585d6..3fb8f19a1db 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -15,13 +15,15 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.storage import Store from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import DATA_COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENTS_COORDINATOR -PLATFORMS = ["alarm_control_panel", "binary_sensor"] +PLATFORMS = ["alarm_control_panel", "binary_sensor", "sensor"] UNDO_UPDATE_LISTENER = "undo_update_listener" - +LAST_EVENT_STORAGE_VERSION = 1 +LAST_EVENT_TIMESTAMP_KEY = "last_event_timestamp" _LOGGER = logging.getLogger(__name__) @@ -46,12 +48,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) coordinator = RiscoDataUpdateCoordinator(hass, risco, scan_interval) await coordinator.async_refresh() + events_coordinator = RiscoEventsDataUpdateCoordinator( + hass, risco, entry.entry_id, 60 + ) undo_listener = entry.add_update_listener(_update_listener) hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, UNDO_UPDATE_LISTENER: undo_listener, + EVENTS_COORDINATOR: events_coordinator, } for component in PLATFORMS: @@ -105,3 +111,37 @@ class RiscoDataUpdateCoordinator(DataUpdateCoordinator): return await self.risco.get_state() except (CannotConnectError, UnauthorizedError, OperationError) as error: raise UpdateFailed(error) from error + + +class RiscoEventsDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching risco data.""" + + def __init__(self, hass, risco, eid, scan_interval): + """Initialize global risco data updater.""" + self.risco = risco + self._store = Store( + hass, LAST_EVENT_STORAGE_VERSION, f"risco_{eid}_last_event_timestamp" + ) + interval = timedelta(seconds=scan_interval) + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}_events", + update_interval=interval, + ) + + async def _async_update_data(self): + """Fetch data from risco.""" + last_store = await self._store.async_load() or {} + last_timestamp = last_store.get( + LAST_EVENT_TIMESTAMP_KEY, "2020-01-01T00:00:00Z" + ) + try: + events = await self.risco.get_events(last_timestamp, 10) + except (CannotConnectError, UnauthorizedError, OperationError) as error: + raise UpdateFailed(error) from error + + if len(events) > 0: + await self._store.async_save({LAST_EVENT_TIMESTAMP_KEY: events[0].time}) + + return events diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index f66f0d33000..46eb011ba5b 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -8,7 +8,10 @@ from homeassistant.const import ( DOMAIN = "risco" +RISCO_EVENT = "risco_event" + DATA_COORDINATOR = "risco" +EVENTS_COORDINATOR = "risco_events" DEFAULT_SCAN_INTERVAL = 30 diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 4a43365e3af..80b132b0fb2 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", "requirements": [ - "pyrisco==0.2.4" + "pyrisco==0.3.0" ], "codeowners": [ "@OnFreund" diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py new file mode 100644 index 00000000000..3b4ee882b3c --- /dev/null +++ b/homeassistant/components/risco/sensor.py @@ -0,0 +1,96 @@ +"""Sensor for Risco Events.""" +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, EVENTS_COORDINATOR + +CATEGORIES = { + 2: "Alarm", + 4: "Status", + 7: "Trouble", +} +EVENT_ATTRIBUTES = [ + "category_id", + "category_name", + "type_id", + "type_name", + "name", + "text", + "partition_id", + "zone_id", + "user_id", + "group", + "priority", + "raw", +] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors for device.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][EVENTS_COORDINATOR] + sensors = [ + RiscoSensor(coordinator, id, [], name) for id, name in CATEGORIES.items() + ] + sensors.append(RiscoSensor(coordinator, None, CATEGORIES.keys(), "Other")) + async_add_entities(sensors) + + +class RiscoSensor(CoordinatorEntity): + """Sensor for Risco events.""" + + def __init__(self, coordinator, category_id, excludes, name) -> None: + """Initialize sensor.""" + super().__init__(coordinator) + self._event = None + self._category_id = category_id + self._excludes = excludes + self._name = name + + @property + def name(self): + """Return the name of the sensor.""" + return f"Risco {self.coordinator.risco.site_name} {self._name} Events" + + @property + def unique_id(self): + """Return a unique id for this sensor.""" + return f"events_{self._name}_{self.coordinator.risco.site_uuid}" + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self.coordinator.async_add_listener(self._refresh_from_coordinator) + ) + await self.coordinator.async_request_refresh() + + def _refresh_from_coordinator(self): + events = self.coordinator.data + for event in reversed(events): + if event.category_id in self._excludes: + continue + if self._category_id is not None and event.category_id != self._category_id: + continue + + self._event = event + self.async_write_ha_state() + + @property + def state(self): + """Value of sensor.""" + if self._event is None: + return None + + return self._event.time + + @property + def device_state_attributes(self): + """State attributes.""" + if self._event is None: + return None + + return {atr: getattr(self._event, atr, None) for atr in EVENT_ATTRIBUTES} + + @property + def device_class(self): + """Device class of sensor.""" + return DEVICE_CLASS_TIMESTAMP diff --git a/requirements_all.txt b/requirements_all.txt index 9d64d6d952e..d3c6277d8c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1591,7 +1591,7 @@ pyrecswitch==1.0.2 pyrepetier==3.0.5 # homeassistant.components.risco -pyrisco==0.2.4 +pyrisco==0.3.0 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 553e84662eb..51880fb4234 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -766,7 +766,7 @@ pyps4-2ndscreen==1.1.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.2.4 +pyrisco==0.3.0 # homeassistant.components.acer_projector # homeassistant.components.zha diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 424699cbb4c..bf7c971df54 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -11,9 +11,6 @@ from homeassistant.components.alarm_control_panel.const import ( from homeassistant.components.risco import CannotConnectError, UnauthorizedError from homeassistant.components.risco.const import DOMAIN from homeassistant.const import ( - CONF_PASSWORD, - CONF_PIN, - CONF_USERNAME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_CUSTOM_BYPASS, SERVICE_ALARM_ARM_HOME, @@ -30,16 +27,11 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity_component import async_update_entity +from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco + from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry -TEST_CONFIG = { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_PIN: "1234", -} -TEST_SITE_UUID = "test-site-uuid" -TEST_SITE_NAME = "test-site-name" FIRST_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0" SECOND_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_1" @@ -110,28 +102,6 @@ def two_part_alarm(): yield alarm_mock -async def _setup_risco(hass, options={}): - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, options=options) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.risco.RiscoAPI.login", - return_value=True, - ), patch( - "homeassistant.components.risco.RiscoAPI.site_uuid", - new_callable=PropertyMock(return_value=TEST_SITE_UUID), - ), patch( - "homeassistant.components.risco.RiscoAPI.site_name", - new_callable=PropertyMock(return_value=TEST_SITE_NAME), - ), patch( - "homeassistant.components.risco.RiscoAPI.close" - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - async def test_cannot_connect(hass): """Test connection error.""" @@ -171,7 +141,7 @@ async def test_setup(hass, two_part_alarm): assert not registry.async_is_registered(FIRST_ENTITY_ID) assert not registry.async_is_registered(SECOND_ENTITY_ID) - await _setup_risco(hass) + await setup_risco(hass) assert registry.async_is_registered(FIRST_ENTITY_ID) assert registry.async_is_registered(SECOND_ENTITY_ID) @@ -196,7 +166,7 @@ async def _check_state(hass, alarm, property, state, entity_id, partition_id): async def test_states(hass, two_part_alarm): """Test the various alarm states.""" - await _setup_risco(hass, CUSTOM_MAPPING_OPTIONS) + await setup_risco(hass, CUSTOM_MAPPING_OPTIONS) assert hass.states.get(FIRST_ENTITY_ID).state == STATE_UNKNOWN for partition_id, entity_id in {0: FIRST_ENTITY_ID, 1: SECOND_ENTITY_ID}.items(): @@ -278,7 +248,7 @@ async def _call_alarm_service(hass, service, entity_id, **kwargs): async def test_sets_custom_mapping(hass, two_part_alarm): """Test settings the various modes when mapping some states.""" - await _setup_risco(hass, CUSTOM_MAPPING_OPTIONS) + await setup_risco(hass, CUSTOM_MAPPING_OPTIONS) registry = await hass.helpers.entity_registry.async_get_registry() entity = registry.async_get(FIRST_ENTITY_ID) @@ -304,7 +274,7 @@ async def test_sets_custom_mapping(hass, two_part_alarm): async def test_sets_full_custom_mapping(hass, two_part_alarm): """Test settings the various modes when mapping all states.""" - await _setup_risco(hass, FULL_CUSTOM_MAPPING) + await setup_risco(hass, FULL_CUSTOM_MAPPING) registry = await hass.helpers.entity_registry.async_get_registry() entity = registry.async_get(FIRST_ENTITY_ID) @@ -338,7 +308,7 @@ async def test_sets_full_custom_mapping(hass, two_part_alarm): async def test_sets_with_correct_code(hass, two_part_alarm): """Test settings the various modes when code is required.""" - await _setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) + await setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) code = {"code": 1234} await _test_service_call( @@ -380,7 +350,7 @@ async def test_sets_with_correct_code(hass, two_part_alarm): async def test_sets_with_incorrect_code(hass, two_part_alarm): """Test settings the various modes when code is required and incorrect.""" - await _setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) + await setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) code = {"code": 4321} await _test_no_service_call( diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index ab7934e523c..9aa56f64e51 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -3,25 +3,14 @@ import pytest from homeassistant.components.risco import CannotConnectError, UnauthorizedError from homeassistant.components.risco.const import DOMAIN -from homeassistant.const import ( - CONF_PASSWORD, - CONF_PIN, - CONF_USERNAME, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity_component import async_update_entity +from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco + from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry -TEST_CONFIG = { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - CONF_PIN: "1234", -} -TEST_SITE_UUID = "test-site-uuid" -TEST_SITE_NAME = "test-site-name" FIRST_ENTITY_ID = "binary_sensor.zone_0" SECOND_ENTITY_ID = "binary_sensor.zone_1" @@ -57,28 +46,6 @@ def two_zone_alarm(): yield alarm_mock -async def _setup_risco(hass): - config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.risco.RiscoAPI.login", - return_value=True, - ), patch( - "homeassistant.components.risco.RiscoAPI.site_uuid", - new_callable=PropertyMock(return_value=TEST_SITE_UUID), - ), patch( - "homeassistant.components.risco.RiscoAPI.site_name", - new_callable=PropertyMock(return_value=TEST_SITE_NAME), - ), patch( - "homeassistant.components.risco.RiscoAPI.close" - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - async def test_cannot_connect(hass): """Test connection error.""" @@ -118,7 +85,7 @@ async def test_setup(hass, two_zone_alarm): assert not registry.async_is_registered(FIRST_ENTITY_ID) assert not registry.async_is_registered(SECOND_ENTITY_ID) - await _setup_risco(hass) + await setup_risco(hass) assert registry.async_is_registered(FIRST_ENTITY_ID) assert registry.async_is_registered(SECOND_ENTITY_ID) @@ -153,7 +120,7 @@ async def _check_state(hass, alarm, triggered, bypassed, entity_id, zone_id): async def test_states(hass, two_zone_alarm): """Test the various alarm states.""" - await _setup_risco(hass) + await setup_risco(hass) await _check_state(hass, two_zone_alarm, True, True, FIRST_ENTITY_ID, 0) await _check_state(hass, two_zone_alarm, True, False, FIRST_ENTITY_ID, 0) @@ -167,7 +134,7 @@ async def test_states(hass, two_zone_alarm): async def test_bypass(hass, two_zone_alarm): """Test bypassing a zone.""" - await _setup_risco(hass) + await setup_risco(hass) with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} @@ -180,7 +147,7 @@ async def test_bypass(hass, two_zone_alarm): async def test_unbypass(hass, two_zone_alarm): """Test unbypassing a zone.""" - await _setup_risco(hass) + await setup_risco(hass) with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py new file mode 100644 index 00000000000..cc76c0b970b --- /dev/null +++ b/tests/components/risco/test_sensor.py @@ -0,0 +1,207 @@ +"""Tests for the Risco event sensors.""" +import pytest + +from homeassistant.components.risco import ( + LAST_EVENT_TIMESTAMP_KEY, + CannotConnectError, + UnauthorizedError, +) +from homeassistant.components.risco.const import DOMAIN, EVENTS_COORDINATOR + +from .util import TEST_CONFIG, setup_risco + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry + +ENTITY_IDS = { + "Alarm": "sensor.risco_test_site_name_alarm_events", + "Status": "sensor.risco_test_site_name_status_events", + "Trouble": "sensor.risco_test_site_name_trouble_events", + "Other": "sensor.risco_test_site_name_other_events", +} + +TEST_EVENTS = [ + MagicMock( + time="2020-09-02T10:00:00Z", + category_id=4, + category_name="System Status", + type_id=16, + type_name="disarmed", + name="'user' disarmed 'partition'", + text="", + partition_id=0, + zone_id=None, + user_id=3, + group=None, + priority=2, + raw={}, + ), + MagicMock( + time="2020-09-02T09:00:00Z", + category_id=7, + category_name="Troubles", + type_id=36, + type_name="service needed", + name="Device Fault", + text="Service is needed.", + partition_id=None, + zone_id=None, + user_id=None, + group=None, + priority=1, + raw={}, + ), + MagicMock( + time="2020-09-02T08:00:00Z", + category_id=2, + category_name="Alarms", + type_id=3, + type_name="triggered", + name="Alarm is on", + text="Yes it is.", + partition_id=0, + zone_id=12, + user_id=None, + group=None, + priority=0, + raw={}, + ), + MagicMock( + time="2020-09-02T07:00:00Z", + category_id=4, + category_name="System Status", + type_id=119, + type_name="group arm", + name="You armed a group", + text="", + partition_id=0, + zone_id=None, + user_id=1, + group="C", + priority=2, + raw={}, + ), + MagicMock( + time="2020-09-02T06:00:00Z", + category_id=8, + category_name="Made up", + type_id=200, + type_name="also made up", + name="really made up", + text="", + partition_id=2, + zone_id=None, + user_id=1, + group=None, + priority=2, + raw={}, + ), +] + +CATEGORIES_TO_EVENTS = { + "Alarm": 2, + "Status": 0, + "Trouble": 1, + "Other": 4, +} + + +@pytest.fixture +def emptry_alarm(): + """Fixture to mock an empty alarm.""" + with patch( + "homeassistant.components.risco.RiscoAPI.get_state", + return_value=MagicMock(paritions={}, zones={}), + ): + yield + + +async def test_cannot_connect(hass): + """Test connection error.""" + + with patch( + "homeassistant.components.risco.RiscoAPI.login", + side_effect=CannotConnectError, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + registry = await hass.helpers.entity_registry.async_get_registry() + for id in ENTITY_IDS.values(): + assert not registry.async_is_registered(id) + + +async def test_unauthorized(hass): + """Test unauthorized error.""" + + with patch( + "homeassistant.components.risco.RiscoAPI.login", + side_effect=UnauthorizedError, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + registry = await hass.helpers.entity_registry.async_get_registry() + for id in ENTITY_IDS.values(): + assert not registry.async_is_registered(id) + + +def _check_state(hass, category, entity_id): + event = TEST_EVENTS[CATEGORIES_TO_EVENTS[category]] + assert hass.states.get(entity_id).state == event.time + assert hass.states.get(entity_id).attributes["category_id"] == event.category_id + assert hass.states.get(entity_id).attributes["category_name"] == event.category_name + assert hass.states.get(entity_id).attributes["type_id"] == event.type_id + assert hass.states.get(entity_id).attributes["type_name"] == event.type_name + assert hass.states.get(entity_id).attributes["name"] == event.name + assert hass.states.get(entity_id).attributes["text"] == event.text + assert hass.states.get(entity_id).attributes["partition_id"] == event.partition_id + assert hass.states.get(entity_id).attributes["zone_id"] == event.zone_id + assert hass.states.get(entity_id).attributes["user_id"] == event.user_id + assert hass.states.get(entity_id).attributes["group"] == event.group + assert hass.states.get(entity_id).attributes["priority"] == event.priority + assert hass.states.get(entity_id).attributes["raw"] == event.raw + + +async def test_setup(hass, emptry_alarm): + """Test entity setup.""" + registry = await hass.helpers.entity_registry.async_get_registry() + + for id in ENTITY_IDS.values(): + assert not registry.async_is_registered(id) + + with patch( + "homeassistant.components.risco.RiscoAPI.get_events", + return_value=TEST_EVENTS, + ), patch( + "homeassistant.components.risco.Store.async_save", + ) as save_mock: + entry = await setup_risco(hass) + await hass.async_block_till_done() + save_mock.assert_awaited_once_with( + {LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time} + ) + + for id in ENTITY_IDS.values(): + assert registry.async_is_registered(id) + + for category, entity_id in ENTITY_IDS.items(): + _check_state(hass, category, entity_id) + + coordinator = hass.data[DOMAIN][entry.entry_id][EVENTS_COORDINATOR] + with patch( + "homeassistant.components.risco.RiscoAPI.get_events", return_value=[] + ) as events_mock, patch( + "homeassistant.components.risco.Store.async_load", + return_value={LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}, + ): + await coordinator.async_refresh() + await hass.async_block_till_done() + events_mock.assert_awaited_once_with(TEST_EVENTS[0].time, 10) + + for category, entity_id in ENTITY_IDS.items(): + _check_state(hass, category, entity_id) diff --git a/tests/components/risco/util.py b/tests/components/risco/util.py new file mode 100644 index 00000000000..a60be70e861 --- /dev/null +++ b/tests/components/risco/util.py @@ -0,0 +1,37 @@ +"""Utilities for Risco tests.""" +from homeassistant.components.risco.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME + +from tests.async_mock import PropertyMock, patch +from tests.common import MockConfigEntry + +TEST_CONFIG = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PIN: "1234", +} +TEST_SITE_UUID = "test-site-uuid" +TEST_SITE_NAME = "test-site-name" + + +async def setup_risco(hass, options={}): + """Set up a Risco integration for testing.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, options=options) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.risco.RiscoAPI.login", + return_value=True, + ), patch( + "homeassistant.components.risco.RiscoAPI.site_uuid", + new_callable=PropertyMock(return_value=TEST_SITE_UUID), + ), patch( + "homeassistant.components.risco.RiscoAPI.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.RiscoAPI.close" + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry From 1fb70625a38131faade95d16d7cc264945d47bfa Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 4 Sep 2020 14:22:50 -0500 Subject: [PATCH 648/862] Prevent mpchc from spamming logs (#39663) --- homeassistant/components/mpchc/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index c09ab685597..8964ea7ed6f 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -82,7 +82,9 @@ class MpcHcDevice(MediaPlayerEntity): self._player_variables[var[0]] = var[1].lower() self._available = True except requests.exceptions.RequestException: - _LOGGER.error("Could not connect to MPC-HC at: %s", self._url) + if self.available: + _LOGGER.error("Could not connect to MPC-HC at: %s", self._url) + self._player_variables = {} self._available = False From 4885b22cb4c6773ca3f01dc03ede7610ba84eba1 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Fri, 4 Sep 2020 15:52:47 -0400 Subject: [PATCH 649/862] Improve ozw websocket response when node is not found (#39653) * Return not_found error over ws if node is not found * Remove unrelated code from this PR * Only import ERR_NOT_FOUND from core websocket_api --- homeassistant/components/ozw/websocket_api.py | 18 ++++++++++++++++++ tests/components/ozw/test_websocket_api.py | 19 +++++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index a71723ed086..da9c2b1f598 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -150,6 +150,15 @@ def websocket_node_status(hass, connection, msg): """Get the status for a Z-Wave node.""" manager = hass.data[DOMAIN][MANAGER] node = manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]) + + if not node: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "OZW Node not found" + ) + ) + return + connection.send_result( msg[ID], { @@ -185,6 +194,15 @@ def websocket_node_metadata(hass, connection, msg): """Get the metadata for a Z-Wave node.""" manager = hass.data[DOMAIN][MANAGER] node = manager.get_instance(msg[OZW_INSTANCE]).get_node(msg[NODE_ID]) + + if not node: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "OZW Node not found" + ) + ) + return + connection.send_result( msg[ID], { diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index 5a8e11a77c8..353615c1812 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -19,6 +19,7 @@ from homeassistant.components.ozw.websocket_api import ( OZW_INSTANCE, TYPE, ) +from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from .common import MQTTMessage, setup_ozw @@ -68,8 +69,13 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): assert result[ATTR_NODE_SPECIFIC_STRING] == "Binary Power Switch" assert result[ATTR_NEIGHBORS] == [1, 33, 36, 37, 39] + await client.send_json({ID: 7, TYPE: "ozw/node_status", NODE_ID: 999}) + msg = await client.receive_json() + result = msg["error"] + assert result["code"] == ERR_NOT_FOUND + # Test node statistics - await client.send_json({ID: 7, TYPE: "ozw/node_statistics", NODE_ID: 39}) + await client.send_json({ID: 8, TYPE: "ozw/node_statistics", NODE_ID: 39}) msg = await client.receive_json() result = msg["result"] @@ -87,13 +93,18 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): assert result["received_unsolicited"] == 3546 # Test node metadata - await client.send_json({ID: 8, TYPE: "ozw/node_metadata", NODE_ID: 39}) + await client.send_json({ID: 9, TYPE: "ozw/node_metadata", NODE_ID: 39}) msg = await client.receive_json() result = msg["result"] assert result["metadata"]["ProductPic"] == "images/aeotec/zwa002.png" + await client.send_json({ID: 10, TYPE: "ozw/node_metadata", NODE_ID: 999}) + msg = await client.receive_json() + result = msg["error"] + assert result["code"] == ERR_NOT_FOUND + # Test network statistics - await client.send_json({ID: 9, TYPE: "ozw/network_statistics"}) + await client.send_json({ID: 11, TYPE: "ozw/network_statistics"}) msg = await client.receive_json() result = msg["result"] assert result["readCnt"] == 92220 @@ -101,7 +112,7 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): assert result["node_count"] == 5 # Test get nodes - await client.send_json({ID: 10, TYPE: "ozw/get_nodes"}) + await client.send_json({ID: 12, TYPE: "ozw/get_nodes"}) msg = await client.receive_json() result = msg["result"] assert len(result) == 5 From 1cb60dd5c79e49470156426cfaaab95fc325fb2d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 4 Sep 2020 22:12:31 +0200 Subject: [PATCH 650/862] Replace old source prefixing for channels with new media browser (#39596) * Replace old source prefixing for channels with new media browser * Restore support to call select source with prefix * Drop warning log for deprecated call method --- .../components/philips_js/media_player.py | 52 ++++++++++++------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index d477c7751cb..9137a61d835 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, @@ -18,6 +19,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) +from homeassistant.components.media_player.errors import BrowseError from homeassistant.const import ( CONF_API_VERSION, CONF_HOST, @@ -40,6 +42,7 @@ SUPPORT_PHILIPS_JS = ( | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY_MEDIA + | SUPPORT_BROWSE_MEDIA ) CONF_ON_ACTION = "turn_on_action" @@ -146,38 +149,28 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): @property def source(self): """Return the current input source.""" - if self.media_content_type == MEDIA_TYPE_CHANNEL: - name = self._channels.get(self._tv.channel_id) - prefix = PREFIX_CHANNEL - else: - name = self._sources.get(self._tv.source_id) - prefix = PREFIX_SOURCE - - if name is None: - return None - return prefix + PREFIX_SEPARATOR + name + return self._sources.get(self._tv.source_id) @property def source_list(self): """List of available input sources.""" - complete = [] - for source in self._sources.values(): - complete.append(PREFIX_SOURCE + PREFIX_SEPARATOR + source) - for channel in self._channels.values(): - complete.append(PREFIX_CHANNEL + PREFIX_SEPARATOR + channel) - return complete + return list(self._sources.values()) def select_source(self, source): """Set the input source.""" data = source.split(PREFIX_SEPARATOR, 1) - if data[0] == PREFIX_SOURCE: + if data[0] == PREFIX_SOURCE: # Legacy way to set source source_id = _inverted(self._sources).get(data[1]) if source_id: self._tv.setSource(source_id) - elif data[0] == PREFIX_CHANNEL: + elif data[0] == PREFIX_CHANNEL: # Legacy way to set channel channel_id = _inverted(self._channels).get(data[1]) if channel_id: self._tv.setChannel(channel_id) + else: + source_id = _inverted(self._sources).get(source) + if source_id: + self._tv.setSource(source_id) self._update_soon(DELAY_ACTION_DEFAULT) @property @@ -281,6 +274,29 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): else: _LOGGER.error("Unsupported media type <%s>", media_type) + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + if media_content_id not in (None, ""): + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) + + return { + "title": "Channels", + "media_content_id": "", + "media_content_type": "library", + "can_play": False, + "children": [ + { + "title": channel, + "media_content_id": channel, + "media_content_type": MEDIA_TYPE_CHANNEL, + "can_play": True, + } + for channel in self._channels.values() + ], + } + def update(self): """Get the latest data and update device state.""" self._tv.update() From 01bac9f433f26440eff3a4f1365cda9480dc971c Mon Sep 17 00:00:00 2001 From: Andrew Marks Date: Fri, 4 Sep 2020 16:13:11 -0400 Subject: [PATCH 651/862] Refactor sharkiq tests (#39564) * Refactor sharkiq tests * Fix linting * Remove unussed logger * Test one more code branch * Don't patch integration files * Remove legacy calls * Linting fixes * Refactor coordinator update tests * Reformat test params * Refector config flow tests * Minor code cleanup * Fix spelling error * Address review * Minor formatting change * Remove vacuum.py from .coveragerc --- .coveragerc | 1 - homeassistant/components/sharkiq/__init__.py | 2 +- tests/components/sharkiq/const.py | 1 + tests/components/sharkiq/test_config_flow.py | 118 +++----- tests/components/sharkiq/test_shark_iq.py | 267 ------------------- tests/components/sharkiq/test_vacuum.py | 242 +++++++++++++++++ 6 files changed, 287 insertions(+), 344 deletions(-) delete mode 100644 tests/components/sharkiq/test_shark_iq.py create mode 100644 tests/components/sharkiq/test_vacuum.py diff --git a/.coveragerc b/.coveragerc index 2dabb5e966f..375d7df4d0e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -752,7 +752,6 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py - homeassistant/components/sharkiq/vacuum.py homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index fb61e54f98f..968ee91f8d6 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -60,7 +60,7 @@ async def async_setup_entry(hass, config_entry): shark_vacs = await ayla_api.async_get_devices(False) device_names = ", ".join([d.name for d in shark_vacs]) - LOGGER.debug("Found %d Shark IQ device(s): %s", len(device_names), device_names) + LOGGER.debug("Found %d Shark IQ device(s): %s", len(shark_vacs), device_names) coordinator = SharkIqUpdateCoordinator(hass, config_entry, ayla_api, shark_vacs) await coordinator.async_refresh() diff --git a/tests/components/sharkiq/const.py b/tests/components/sharkiq/const.py index 392b4a863d3..305d12ddfa7 100644 --- a/tests/components/sharkiq/const.py +++ b/tests/components/sharkiq/const.py @@ -71,3 +71,4 @@ TEST_USERNAME = "test-username" TEST_PASSWORD = "test-password" UNIQUE_ID = "foo@bar.com" CONFIG = {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD} +ENTRY_ID = "0123456789abcdef0123456789abcdef" diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index f588b5f82a2..3183f6fdee2 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -1,24 +1,18 @@ """Test the Shark IQ config flow.""" import aiohttp -from sharkiqpy import SharkIqAuthError +import pytest +from sharkiqpy import AylaApi, SharkIqAuthError from homeassistant import config_entries, setup from homeassistant.components.sharkiq.const import DOMAIN +from homeassistant.core import HomeAssistant from .const import CONFIG, TEST_PASSWORD, TEST_USERNAME, UNIQUE_ID -from tests.async_mock import MagicMock, PropertyMock, patch +from tests.async_mock import patch from tests.common import MockConfigEntry -def _create_mocked_ayla(connect=None): - """Create a mocked AylaApi object.""" - mocked_ayla = MagicMock() - type(mocked_ayla).sign_in = PropertyMock(side_effect=connect) - type(mocked_ayla).async_sign_in = PropertyMock(side_effect=connect) - return mocked_ayla - - async def test_form(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -46,75 +40,37 @@ async def test_form(hass): "password": TEST_PASSWORD, } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 + mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() -async def test_form_invalid_auth(hass): - """Test we handle invalid auth.""" +@pytest.mark.parametrize( + "exc,base_error", + [ + (SharkIqAuthError, "invalid_auth"), + (aiohttp.ClientError, "cannot_connect"), + (TypeError, "unknown"), + ], +) +async def test_form_error(hass: HomeAssistant, exc: Exception, base_error: str): + """Test form errors.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mocked_ayla = _create_mocked_ayla(connect=SharkIqAuthError) - with patch( - "homeassistant.components.sharkiq.config_flow.get_ayla_api", - return_value=mocked_ayla, - ): + with patch.object(AylaApi, "async_sign_in", side_effect=exc): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["errors"].get("base") == base_error -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - mocked_ayla = _create_mocked_ayla(connect=aiohttp.ClientError) - - with patch( - "homeassistant.components.sharkiq.config_flow.get_ayla_api", - return_value=mocked_ayla, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_other_error(hass): - """Test we handle other errors.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - mocked_ayla = _create_mocked_ayla(connect=TypeError) - - with patch( - "homeassistant.components.sharkiq.config_flow.get_ayla_api", - return_value=mocked_ayla, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} - - -async def test_reauth(hass): +async def test_reauth_success(hass: HomeAssistant): """Test reauth flow.""" - with patch( - "homeassistant.components.sharkiq.vacuum.async_setup_entry", - return_value=True, - ), patch("sharkiqpy.AylaApi.async_sign_in", return_value=True): + with patch("sharkiqpy.AylaApi.async_sign_in", return_value=True): mock_config = MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG) mock_config.add_to_hass(hass) @@ -125,21 +81,33 @@ async def test_reauth(hass): assert result["type"] == "abort" assert result["reason"] == "reauth_successful" - with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=SharkIqAuthError): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": "reauth", "unique_id": UNIQUE_ID}, - data=CONFIG, - ) - assert result["type"] == "form" - assert result["errors"] == {"base": "invalid_auth"} - with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=RuntimeError): +@pytest.mark.parametrize( + "side_effect,result_type,msg_field,msg", + [ + (SharkIqAuthError, "form", "errors", "invalid_auth"), + (aiohttp.ClientError, "abort", "reason", "cannot_connect"), + (TypeError, "abort", "reason", "unknown"), + ], +) +async def test_reauth( + hass: HomeAssistant, + side_effect: Exception, + result_type: str, + msg_field: str, + msg: str, +): + """Test reauth failures.""" + with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=side_effect): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG, ) - assert result["type"] == "abort" - assert result["reason"] == "unknown" + msg_value = result[msg_field] + if msg_field == "errors": + msg_value = msg_value.get("base") + + assert result["type"] == result_type + assert msg_value == msg diff --git a/tests/components/sharkiq/test_shark_iq.py b/tests/components/sharkiq/test_shark_iq.py deleted file mode 100644 index 927f62b138c..00000000000 --- a/tests/components/sharkiq/test_shark_iq.py +++ /dev/null @@ -1,267 +0,0 @@ -"""Test the Shark IQ vacuum entity.""" -from copy import deepcopy -import enum -import json -from typing import Dict, List - -from sharkiqpy import AylaApi, Properties, SharkIqAuthError, SharkIqVacuum, get_ayla_api - -from homeassistant.components.sharkiq import SharkIqUpdateCoordinator -from homeassistant.components.sharkiq.vacuum import ( - ATTR_ERROR_CODE, - ATTR_ERROR_MSG, - ATTR_LOW_LIGHT, - ATTR_RECHARGE_RESUME, - SharkVacuumEntity, -) -from homeassistant.components.vacuum import ( - STATE_CLEANING, - STATE_DOCKED, - STATE_IDLE, - STATE_PAUSED, - STATE_RETURNING, - SUPPORT_BATTERY, - SUPPORT_FAN_SPEED, - SUPPORT_LOCATE, - SUPPORT_PAUSE, - SUPPORT_RETURN_HOME, - SUPPORT_START, - SUPPORT_STATE, - SUPPORT_STATUS, - SUPPORT_STOP, -) -from homeassistant.config_entries import ConfigEntriesFlowManager, ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import UpdateFailed - -from .const import ( - SHARK_DEVICE_DICT, - SHARK_METADATA_DICT, - SHARK_PROPERTIES_DICT, - TEST_PASSWORD, - TEST_USERNAME, -) - -from tests.async_mock import MagicMock, patch - -MockAyla = MagicMock(spec=AylaApi) # pylint: disable=invalid-name - - -def _set_property(self, property_name, value): - """Set a property locally without hitting the API.""" - if isinstance(property_name, enum.Enum): - property_name = property_name.value - if isinstance(value, enum.Enum): - value = value.value - self.properties_full[property_name]["value"] = value - - -async def _async_set_property(self, property_name, value): - """Set a property locally without hitting the API.""" - _set_property(self, property_name, value) - - -def _get_mock_shark_vac(ayla_api: AylaApi) -> SharkIqVacuum: - """Create a crude sharkiq vacuum with mocked properties.""" - shark = SharkIqVacuum(ayla_api, SHARK_DEVICE_DICT) - shark.properties_full = deepcopy(SHARK_PROPERTIES_DICT) - return shark - - -async def _async_list_devices(_) -> List[Dict]: - """Generate a dummy of async_list_devices output.""" - return [SHARK_DEVICE_DICT] - - -@patch.object(SharkIqVacuum, "set_property_value", new=_set_property) -@patch.object(SharkIqVacuum, "async_set_property_value", new=_async_set_property) -async def test_shark_operation_modes(hass: HomeAssistant) -> None: - """Test all of the shark vacuum operation modes.""" - ayla_api = MockAyla() - shark_vac = _get_mock_shark_vac(ayla_api) - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac]) - shark = SharkVacuumEntity(shark_vac, coordinator) - - # These come from the setup - assert isinstance(shark.is_docked, bool) and not shark.is_docked - assert ( - isinstance(shark.recharging_to_resume, bool) and not shark.recharging_to_resume - ) - # Go through the operation modes while it's "off the dock" - await shark.async_start() - assert shark.operating_mode == shark.state == STATE_CLEANING - await shark.async_pause() - assert shark.operating_mode == shark.state == STATE_PAUSED - await shark.async_stop() - assert shark.operating_mode == shark.state == STATE_IDLE - await shark.async_return_to_base() - assert shark.operating_mode == shark.state == STATE_RETURNING - - # Test the docked modes - await shark.async_stop() - shark.sharkiq.set_property_value(Properties.RECHARGING_TO_RESUME, 1) - shark.sharkiq.set_property_value(Properties.DOCKED_STATUS, 1) - assert isinstance(shark.is_docked, bool) and shark.is_docked - assert isinstance(shark.recharging_to_resume, bool) and shark.recharging_to_resume - assert shark.state == STATE_DOCKED - - shark.sharkiq.set_property_value(Properties.RECHARGING_TO_RESUME, 0) - assert shark.state == STATE_DOCKED - - await shark.async_set_fan_speed("Eco") - assert shark.fan_speed == "Eco" - await shark.async_set_fan_speed("Max") - assert shark.fan_speed == "Max" - await shark.async_set_fan_speed("Normal") - assert shark.fan_speed == "Normal" - - assert set(shark.fan_speed_list) == {"Normal", "Max", "Eco"} - - -@patch.object(SharkIqVacuum, "set_property_value", new=_set_property) -async def test_shark_vac_properties(hass: HomeAssistant) -> None: - """Test all of the shark vacuum property accessors.""" - ayla_api = MockAyla() - shark_vac = _get_mock_shark_vac(ayla_api) - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac]) - shark = SharkVacuumEntity(shark_vac, coordinator) - - assert shark.name == "Sharknado" - assert shark.serial_number == "AC000Wxxxxxxxxx" - assert shark.model == "RV1000A" - - assert shark.battery_level == 50 - assert shark.fan_speed == "Eco" - shark.sharkiq.set_property_value(Properties.POWER_MODE, 0) - assert shark.fan_speed == "Normal" - assert isinstance(shark.recharge_resume, bool) and shark.recharge_resume - assert isinstance(shark.low_light, bool) and not shark.low_light - - target_state_attributes = { - ATTR_ERROR_CODE: 7, - ATTR_ERROR_MSG: "Cliff sensor is blocked", - ATTR_RECHARGE_RESUME: True, - ATTR_LOW_LIGHT: False, - } - state_json = json.dumps(shark.device_state_attributes, sort_keys=True) - target_json = json.dumps(target_state_attributes, sort_keys=True) - assert state_json == target_json - - assert not shark.should_poll - - -@patch.object(SharkIqVacuum, "set_property_value", new=_set_property) -@patch.object(SharkIqVacuum, "async_set_property_value", new=_async_set_property) -async def test_shark_metadata(hass: HomeAssistant) -> None: - """Test shark properties coming from metadata.""" - ayla_api = MockAyla() - shark_vac = _get_mock_shark_vac(ayla_api) - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac]) - shark = SharkVacuumEntity(shark_vac, coordinator) - shark.sharkiq._update_metadata( # pylint: disable=protected-access - SHARK_METADATA_DICT - ) - - target_device_info = { - "identifiers": {("sharkiq", "AC000Wxxxxxxxxx")}, - "name": "Sharknado", - "manufacturer": "Shark", - "model": "RV1001AE", - "sw_version": "Dummy Firmware 1.0", - } - - assert shark.device_info == target_device_info - - -def _get_async_update(err=None): - async def _async_update(_) -> bool: - if err is not None: - raise err - return True - - return _async_update - - -@patch.object(AylaApi, "async_list_devices", new=_async_list_devices) -async def test_updates(hass: HomeAssistant) -> None: - """Test the update coordinator update functions.""" - ayla_api = get_ayla_api(TEST_USERNAME, TEST_PASSWORD) - shark_vac = _get_mock_shark_vac(ayla_api) - mock_config = MagicMock(spec=ConfigEntry) - coordinator = SharkIqUpdateCoordinator(hass, mock_config, ayla_api, [shark_vac]) - - with patch.object(SharkIqVacuum, "async_update", new=_get_async_update()): - update_called = ( - await coordinator._async_update_data() # pylint: disable=protected-access - ) - assert update_called - - update_failed = False - with patch.object( - SharkIqVacuum, "async_update", new=_get_async_update(SharkIqAuthError) - ), patch.object(HomeAssistant, "async_create_task"), patch.object( - ConfigEntriesFlowManager, "async_init" - ): - try: - await coordinator._async_update_data() # pylint: disable=protected-access - except UpdateFailed: - update_failed = True - assert update_failed - - -async def test_coordinator_match(hass: HomeAssistant): - """Test that sharkiq-coordinator references work.""" - ayla_api = get_ayla_api(TEST_PASSWORD, TEST_USERNAME) - shark_vac1 = _get_mock_shark_vac(ayla_api) - shark_vac2 = _get_mock_shark_vac(ayla_api) - shark_vac2._dsn = "FOOBAR!" # pylint: disable=protected-access - - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac1]) - - api = SharkVacuumEntity(shark_vac1, coordinator) - coordinator.last_update_success = True - coordinator._online_dsns = set() # pylint: disable=protected-access - assert not api.is_online - assert not api.available - - coordinator._online_dsns = { # pylint: disable=protected-access - shark_vac1.serial_number - } - assert api.is_online - assert api.available - - coordinator.last_update_success = False - assert not api.available - - -async def test_simple_properties(hass: HomeAssistant): - """Test that simple properties work as intended.""" - ayla_api = get_ayla_api(TEST_PASSWORD, TEST_USERNAME) - shark_vac1 = _get_mock_shark_vac(ayla_api) - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac1]) - entity = SharkVacuumEntity(shark_vac1, coordinator) - - assert entity.unique_id == "AC000Wxxxxxxxxx" - - assert entity.supported_features == ( - SUPPORT_BATTERY - | SUPPORT_FAN_SPEED - | SUPPORT_PAUSE - | SUPPORT_RETURN_HOME - | SUPPORT_START - | SUPPORT_STATE - | SUPPORT_STATUS - | SUPPORT_STOP - | SUPPORT_LOCATE - ) - - assert entity.error_code == 7 - assert entity.error_message == "Cliff sensor is blocked" - shark_vac1.properties_full[Properties.ERROR_CODE.value]["value"] = 0 - assert entity.error_code == 0 - assert entity.error_message is None - - assert ( - coordinator.online_dsns - is coordinator._online_dsns # pylint: disable=protected-access - ) diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py new file mode 100644 index 00000000000..3b40011b8e6 --- /dev/null +++ b/tests/components/sharkiq/test_vacuum.py @@ -0,0 +1,242 @@ +"""Test the Shark IQ vacuum entity.""" +from copy import deepcopy +import enum +from typing import Any, Iterable, List, Optional + +import pytest +from sharkiqpy import AylaApi, SharkIqAuthError, SharkIqVacuum + +from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY +from homeassistant.components.sharkiq import DOMAIN +from homeassistant.components.sharkiq.vacuum import ( + ATTR_ERROR_CODE, + ATTR_ERROR_MSG, + ATTR_LOW_LIGHT, + ATTR_RECHARGE_RESUME, + FAN_SPEEDS_MAP, +) +from homeassistant.components.vacuum import ( + ATTR_BATTERY_LEVEL, + ATTR_FAN_SPEED, + ATTR_FAN_SPEED_LIST, + SERVICE_LOCATE, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, + STATE_CLEANING, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, + SUPPORT_BATTERY, + SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, + SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STATUS, + SUPPORT_STOP, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import ( + CONFIG, + ENTRY_ID, + SHARK_DEVICE_DICT, + SHARK_METADATA_DICT, + SHARK_PROPERTIES_DICT, + TEST_USERNAME, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +VAC_ENTITY_ID = f"vacuum.{SHARK_DEVICE_DICT['product_name'].lower()}" +EXPECTED_FEATURES = ( + SUPPORT_BATTERY + | SUPPORT_FAN_SPEED + | SUPPORT_PAUSE + | SUPPORT_RETURN_HOME + | SUPPORT_START + | SUPPORT_STATE + | SUPPORT_STATUS + | SUPPORT_STOP + | SUPPORT_LOCATE +) + + +class MockAyla(AylaApi): + """Mocked AylaApi that doesn't do anything.""" + + async def async_sign_in(self): + """Instead of signing in, just return.""" + + async def async_list_devices(self) -> List[dict]: + """Return the device list.""" + return [SHARK_DEVICE_DICT] + + async def async_get_devices(self, update: bool = True) -> List[SharkIqVacuum]: + """Get the list of devices.""" + shark = MockShark(self, SHARK_DEVICE_DICT) + shark.properties_full = deepcopy(SHARK_PROPERTIES_DICT) + shark._update_metadata(SHARK_METADATA_DICT) # pylint: disable=protected-access + return [shark] + + async def async_request(self, http_method: str, url: str, **kwargs): + """Don't make an HTTP request.""" + + +class MockShark(SharkIqVacuum): + """Mocked SharkIqVacuum that won't hit the API.""" + + async def async_update(self, property_list: Optional[Iterable[str]] = None): + """Don't do anything.""" + + def set_property_value(self, property_name, value): + """Set a property locally without hitting the API.""" + if isinstance(property_name, enum.Enum): + property_name = property_name.value + if isinstance(value, enum.Enum): + value = value.value + self.properties_full[property_name]["value"] = value + + async def async_set_property_value(self, property_name, value): + """Set a property locally without hitting the API.""" + self.set_property_value(property_name, value) + + +@pytest.fixture(autouse=True) +@patch("sharkiqpy.ayla_api.AylaApi", MockAyla) +async def setup_integration(hass): + """Build the mock integration.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id=TEST_USERNAME, data=CONFIG, entry_id=ENTRY_ID + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +async def test_simple_properties(hass: HomeAssistant): + """Test that simple properties work as intended.""" + state = hass.states.get(VAC_ENTITY_ID) + registry = await hass.helpers.entity_registry.async_get_registry() + entity = registry.async_get(VAC_ENTITY_ID) + + assert entity + assert state + assert state.state == STATE_CLEANING + assert entity.unique_id == "AC000Wxxxxxxxxx" + + +@pytest.mark.parametrize( + "attribute,target_value", + [ + (ATTR_SUPPORTED_FEATURES, EXPECTED_FEATURES), + (ATTR_BATTERY_LEVEL, 50), + (ATTR_FAN_SPEED, "Eco"), + (ATTR_FAN_SPEED_LIST, list(FAN_SPEEDS_MAP)), + (ATTR_ERROR_CODE, 7), + (ATTR_ERROR_MSG, "Cliff sensor is blocked"), + (ATTR_LOW_LIGHT, False), + (ATTR_RECHARGE_RESUME, True), + ], +) +async def test_initial_attributes( + hass: HomeAssistant, attribute: str, target_value: Any +): + """Test initial config attributes.""" + state = hass.states.get(VAC_ENTITY_ID) + assert state.attributes.get(attribute) == target_value + + +@pytest.mark.parametrize( + "service,target_state", + [ + (SERVICE_STOP, STATE_IDLE), + (SERVICE_PAUSE, STATE_PAUSED), + (SERVICE_RETURN_TO_BASE, STATE_RETURNING), + (SERVICE_START, STATE_CLEANING), + ], +) +async def test_cleaning_states(hass: HomeAssistant, service: str, target_state: str): + """Test cleaning states.""" + service_data = {ATTR_ENTITY_ID: VAC_ENTITY_ID} + await hass.services.async_call("vacuum", service, service_data, blocking=True) + state = hass.states.get(VAC_ENTITY_ID) + assert state.state == target_state + + +@pytest.mark.parametrize("fan_speed", list(FAN_SPEEDS_MAP)) +async def test_fan_speed(hass: HomeAssistant, fan_speed: str) -> None: + """Test setting fan speeds.""" + service_data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_FAN_SPEED: fan_speed} + await hass.services.async_call( + "vacuum", SERVICE_SET_FAN_SPEED, service_data, blocking=True + ) + state = hass.states.get(VAC_ENTITY_ID) + assert state.attributes.get(ATTR_FAN_SPEED) == fan_speed + + +@pytest.mark.parametrize( + "device_property,target_value", + [ + ("manufacturer", "Shark"), + ("model", "RV1001AE"), + ("name", "Sharknado"), + ("sw_version", "Dummy Firmware 1.0"), + ], +) +async def test_device_properties( + hass: HomeAssistant, device_property: str, target_value: str +): + """Test device properties.""" + registry = await hass.helpers.device_registry.async_get_registry() + device = registry.async_get_device({(DOMAIN, "AC000Wxxxxxxxxx")}, []) + assert getattr(device, device_property) == target_value + + +async def test_locate(hass): + """Test that the locate command works.""" + with patch.object(SharkIqVacuum, "async_find_device") as mock_locate: + data = {ATTR_ENTITY_ID: VAC_ENTITY_ID} + await hass.services.async_call("vacuum", SERVICE_LOCATE, data, blocking=True) + mock_locate.assert_called_once() + + +@pytest.mark.parametrize( + "side_effect,success", + [ + (None, True), + (SharkIqAuthError, False), + (RuntimeError, False), + ], +) +async def test_coordinator_updates( + hass: HomeAssistant, side_effect: Optional[Exception], success: bool +) -> None: + """Test the update coordinator update functions.""" + coordinator = hass.data[DOMAIN][ENTRY_ID] + + await async_setup_component(hass, "homeassistant", {}) + + with patch.object( + MockShark, "async_update", side_effect=side_effect + ) as mock_update: + data = {ATTR_ENTITY_ID: [VAC_ENTITY_ID]} + await hass.services.async_call( + "homeassistant", SERVICE_UPDATE_ENTITY, data, blocking=True + ) + assert coordinator.last_update_success == success + mock_update.assert_called_once() + + state = hass.states.get(VAC_ENTITY_ID) + assert (state.state == STATE_UNAVAILABLE) != success From 0d3396ed64e5407987469162f5cb5ce561445f8a Mon Sep 17 00:00:00 2001 From: RogerSelwyn Date: Fri, 4 Sep 2020 21:31:06 +0100 Subject: [PATCH 652/862] Add mac address to sky_hub tracker (#39506) * Add mac address to Sky_Hub tracker * Update manifest.json * Update device_tracker.py * Update device_tracker.py * Update device_tracker.py * Apply suggestions from code review Co-authored-by: Chris Talkington --- .../components/sky_hub/device_tracker.py | 18 ++++++++++++++++-- homeassistant/components/sky_hub/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index b97331b6195..75241d78ed3 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -44,11 +44,25 @@ class SkyHubDeviceScanner(DeviceScanner): async def async_scan_devices(self): """Scan for new devices and return a list with found device IDs.""" await self._async_update_info() - return list(self.last_results) + return [device.mac for device in self.last_results] async def async_get_device_name(self, device): """Return the name of the given device.""" - return self.last_results.get(device) + name = next( + (result.name for result in self.last_results if result.mac == device), + None, + ) + return name + + async def async_get_extra_attributes(self, device): + """Get extra attributes of a device.""" + device = next( + (result for result in self.last_results if result.mac == device), None + ) + if device is None: + return {} + + return device.asdict() async def _async_update_info(self): """Ensure the information from the Sky Hub is up to date.""" diff --git a/homeassistant/components/sky_hub/manifest.json b/homeassistant/components/sky_hub/manifest.json index e663820a5ef..a0ef4bc8e0c 100644 --- a/homeassistant/components/sky_hub/manifest.json +++ b/homeassistant/components/sky_hub/manifest.json @@ -2,6 +2,6 @@ "domain": "sky_hub", "name": "Sky Hub", "documentation": "https://www.home-assistant.io/integrations/sky_hub", - "requirements": ["pyskyqhub==0.1.1"], + "requirements": ["pyskyqhub==0.1.2"], "codeowners": ["@rogerselwyn"] } diff --git a/requirements_all.txt b/requirements_all.txt index d3c6277d8c9..8c925a1c621 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1622,7 +1622,7 @@ pysher==1.0.1 pysignalclirestapi==0.3.4 # homeassistant.components.sky_hub -pyskyqhub==0.1.1 +pyskyqhub==0.1.2 # homeassistant.components.sma pysma==0.3.5 From ab2d171909de0d4c91959c326b24d2bf8750d37a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 5 Sep 2020 00:23:20 +0200 Subject: [PATCH 653/862] Updated frontend to 20200904.0 (#39665) --- 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 6e1836a5c63..377381e03c7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200901.0"], + "requirements": ["home-assistant-frontend==20200904.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5fc91bc5dbc..a21eade4aaa 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.36.1 -home-assistant-frontend==20200901.0 +home-assistant-frontend==20200904.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 8c925a1c621..5397f3a9cb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -746,7 +746,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200901.0 +home-assistant-frontend==20200904.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51880fb4234..ca94a516236 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200901.0 +home-assistant-frontend==20200904.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 706f1f8a66e3453a2df371c3c762db9ac73cfa47 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 4 Sep 2020 18:28:53 -0400 Subject: [PATCH 654/862] Register media browser panel (#39655) * register media browser panel * Apply suggestions from code review. Co-authored-by: Zack Arnett * Update homeassistant/components/media_source/__init__.py Co-authored-by: Zack Arnett Co-authored-by: Zack Arnett --- homeassistant/components/media_source/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 763baa99b41..6dc4eecc6dc 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -37,6 +37,9 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.data[DOMAIN] = {} hass.components.websocket_api.async_register_command(websocket_browse_media) hass.components.websocket_api.async_register_command(websocket_resolve_media) + hass.components.frontend.async_register_built_in_panel( + "media-browser", "media-browser", "hass:play-box-multiple" + ) local_source.async_setup(hass) await async_process_integration_platforms( hass, DOMAIN, _process_media_source_platform From 8b4847162d65dff9363d0ac4a3121f443e9cccc6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 5 Sep 2020 01:01:34 +0200 Subject: [PATCH 655/862] Bump gios library (#39669) --- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 0fc544c9d1e..99e54edfeaf 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -3,7 +3,7 @@ "name": "GIOŚ", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], - "requirements": ["gios==0.1.3"], + "requirements": ["gios==0.1.4"], "config_flow": true, "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 5397f3a9cb5..acf59902791 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -656,7 +656,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.1.3 +gios==0.1.4 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca94a516236..c5741cc63ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.1.3 +gios==0.1.4 # homeassistant.components.glances glances_api==0.2.0 From 1e770f089d4c596936925da90a8d6216779f87d9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 5 Sep 2020 01:01:49 +0200 Subject: [PATCH 656/862] Bump accuweather library (#39667) --- homeassistant/components/accuweather/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index dd039717468..a383c49f348 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -2,7 +2,7 @@ "domain": "accuweather", "name": "AccuWeather", "documentation": "https://www.home-assistant.io/integrations/accuweather/", - "requirements": ["accuweather==0.0.9"], + "requirements": ["accuweather==0.0.10"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index acf59902791..83d8503bf3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -102,7 +102,7 @@ YesssSMS==0.4.1 abodepy==1.1.0 # homeassistant.components.accuweather -accuweather==0.0.9 +accuweather==0.0.10 # homeassistant.components.mcp23017 adafruit-blinka==3.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5741cc63ff..ff64f2253e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ YesssSMS==0.4.1 abodepy==1.1.0 # homeassistant.components.accuweather -accuweather==0.0.9 +accuweather==0.0.10 # homeassistant.components.androidtv adb-shell[async]==0.2.1 From b0192cf9c0c5b83cfbaf92ee6fa26f5bf9568a8b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 5 Sep 2020 01:22:50 +0200 Subject: [PATCH 657/862] Add OpenWeatherMap config_flow (#34659) Co-authored-by: J. Nick Koston --- .coveragerc | 3 + CODEOWNERS | 2 +- .../components/openweathermap/__init__.py | 127 ++++++++ .../openweathermap/abstract_owm_sensor.py | 81 +++++ .../components/openweathermap/config_flow.py | 138 ++++++++ .../components/openweathermap/const.py | 142 +++++++++ .../forecast_update_coordinator.py | 137 ++++++++ .../components/openweathermap/manifest.json | 5 +- .../components/openweathermap/sensor.py | 282 +++++------------ .../components/openweathermap/strings.json | 35 ++ .../openweathermap/translations/en.json | 35 ++ .../components/openweathermap/weather.py | 299 +++++------------- .../weather_update_coordinator.py | 94 ++++++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/openweathermap/__init__.py | 1 + .../openweathermap/test_config_flow.py | 232 ++++++++++++++ 17 files changed, 1193 insertions(+), 424 deletions(-) create mode 100644 homeassistant/components/openweathermap/abstract_owm_sensor.py create mode 100644 homeassistant/components/openweathermap/config_flow.py create mode 100644 homeassistant/components/openweathermap/const.py create mode 100644 homeassistant/components/openweathermap/forecast_update_coordinator.py create mode 100644 homeassistant/components/openweathermap/strings.json create mode 100644 homeassistant/components/openweathermap/translations/en.json create mode 100644 homeassistant/components/openweathermap/weather_update_coordinator.py create mode 100644 tests/components/openweathermap/__init__.py create mode 100644 tests/components/openweathermap/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 375d7df4d0e..271e1f0c7ec 100644 --- a/.coveragerc +++ b/.coveragerc @@ -623,6 +623,9 @@ omit = homeassistant/components/openuv/sensor.py homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/weather.py + homeassistant/components/openweathermap/forecast_update_coordinator.py + homeassistant/components/openweathermap/weather_update_coordinator.py + homeassistant/components/openweathermap/abstract_owm_sensor.py homeassistant/components/opnsense/* homeassistant/components/opple/light.py homeassistant/components/orangepi_gpio/* diff --git a/CODEOWNERS b/CODEOWNERS index 0caa4be8671..e3b0f0462c3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -305,7 +305,7 @@ homeassistant/components/openerz/* @misialq homeassistant/components/opengarage/* @danielhiversen homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya -homeassistant/components/openweathermap/* @fabaff +homeassistant/components/openweathermap/* @fabaff @freekode homeassistant/components/opnsense/* @mtreinish homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/oru/* @bvlaicu diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 43cad1520ca..bdda75bae29 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -1 +1,128 @@ """The openweathermap component.""" +import asyncio +import logging + +from pyowm import OWM + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import ( + COMPONENTS, + CONF_LANGUAGE, + DOMAIN, + ENTRY_FORECAST_COORDINATOR, + ENTRY_NAME, + ENTRY_WEATHER_COORDINATOR, + UPDATE_LISTENER, +) +from .forecast_update_coordinator import ForecastUpdateCoordinator +from .weather_update_coordinator import WeatherUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the OpenWeatherMap component.""" + hass.data.setdefault(DOMAIN, {}) + + weather_configs = _filter_domain_configs(config.get("weather", []), DOMAIN) + sensor_configs = _filter_domain_configs(config.get("sensor", []), DOMAIN) + + _import_configs(hass, weather_configs + sensor_configs) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Set up OpenWeatherMap as config entry.""" + name = config_entry.data[CONF_NAME] + api_key = config_entry.data[CONF_API_KEY] + latitude = config_entry.data.get(CONF_LATITUDE, hass.config.latitude) + longitude = config_entry.data.get(CONF_LONGITUDE, hass.config.longitude) + forecast_mode = _get_config_value(config_entry, CONF_MODE) + language = _get_config_value(config_entry, CONF_LANGUAGE) + + owm = OWM(API_key=api_key, language=language) + weather_coordinator = WeatherUpdateCoordinator(owm, latitude, longitude, hass) + forecast_coordinator = ForecastUpdateCoordinator( + owm, latitude, longitude, forecast_mode, hass + ) + + await weather_coordinator.async_refresh() + await forecast_coordinator.async_refresh() + + if ( + not weather_coordinator.last_update_success + and not forecast_coordinator.last_update_success + ): + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = { + ENTRY_NAME: name, + ENTRY_WEATHER_COORDINATOR: weather_coordinator, + ENTRY_FORECAST_COORDINATOR: forecast_coordinator, + } + + for component in COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + update_listener = config_entry.add_update_listener(async_update_options) + hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] = update_listener + + return True + + +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in COMPONENTS + ] + ) + ) + if unload_ok: + update_listener = hass.data[DOMAIN][config_entry.entry_id][UPDATE_LISTENER] + update_listener() + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +def _import_configs(hass, configs): + for config in configs: + _LOGGER.debug("Importing OpenWeatherMap %s", config) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + + +def _filter_domain_configs(elements, domain): + return list(filter(lambda elem: elem["platform"] == domain, elements)) + + +def _get_config_value(config_entry, key): + if config_entry.options: + return config_entry.options[key] + return config_entry.data[key] diff --git a/homeassistant/components/openweathermap/abstract_owm_sensor.py b/homeassistant/components/openweathermap/abstract_owm_sensor.py new file mode 100644 index 00000000000..7378324b0a4 --- /dev/null +++ b/homeassistant/components/openweathermap/abstract_owm_sensor.py @@ -0,0 +1,81 @@ +"""Abstraction form OWM sensors.""" +import logging + +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import ATTRIBUTION, SENSOR_DEVICE_CLASS, SENSOR_NAME, SENSOR_UNIT + +_LOGGER = logging.getLogger(__name__) + + +class AbstractOpenWeatherMapSensor(Entity): + """Abstract class for an OpenWeatherMap sensor.""" + + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + coordinator: DataUpdateCoordinator, + ): + """Initialize the sensor.""" + self._name = name + self._unique_id = unique_id + self._sensor_type = sensor_type + self._sensor_name = sensor_configuration[SENSOR_NAME] + self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) + self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) + self._coordinator = coordinator + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {self._sensor_name}" + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def should_poll(self): + """Return the polling requirement of the entity.""" + return False + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def device_class(self): + """Return the device_class.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self): + """Get the latest data from OWM and updates the states.""" + await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py new file mode 100644 index 00000000000..365f55c5d44 --- /dev/null +++ b/homeassistant/components/openweathermap/config_flow.py @@ -0,0 +1,138 @@ +"""Config flow for OpenWeatherMap.""" +import logging + +from pyowm import OWM +from pyowm.exceptions.api_call_error import APICallError +from pyowm.exceptions.api_response_error import UnauthorizedError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_LANGUAGE, + DEFAULT_FORECAST_MODE, + DEFAULT_LANGUAGE, + DEFAULT_NAME, + FORECAST_MODES, + LANGUAGES, +) +from .const import DOMAIN # pylint:disable=unused-import + +SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MODE, default=DEFAULT_FORECAST_MODE): vol.In(FORECAST_MODES), + vol.Optional(CONF_LANGUAGE, default=DEFAULT_LANGUAGE): vol.In(LANGUAGES), + } +) + +_LOGGER = logging.getLogger(__name__) + + +class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for OpenWeatherMap.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OpenWeatherMapOptionsFlow(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + latitude = user_input[CONF_LATITUDE] + longitude = user_input[CONF_LONGITUDE] + + await self.async_set_unique_id(f"{latitude}-{longitude}") + self._abort_if_unique_id_configured() + + try: + api_online = await _is_owm_api_online( + self.hass, user_input[CONF_API_KEY] + ) + if not api_online: + errors["base"] = "auth" + except UnauthorizedError: + errors["base"] = "auth" + except APICallError: + errors["base"] = "connection" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + return self.async_show_form(step_id="user", data_schema=SCHEMA, errors=errors) + + async def async_step_import(self, import_input=None): + """Set the config entry up from yaml.""" + config = import_input.copy() + if CONF_NAME not in config: + config[CONF_NAME] = DEFAULT_NAME + if CONF_LATITUDE not in config: + config[CONF_LATITUDE] = self.hass.config.latitude + if CONF_LONGITUDE not in config: + config[CONF_LONGITUDE] = self.hass.config.longitude + if CONF_MODE not in config: + config[CONF_MODE] = DEFAULT_LANGUAGE + if CONF_LANGUAGE not in config: + config[CONF_LANGUAGE] = DEFAULT_LANGUAGE + return await self.async_step_user(config) + + +class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=self._get_options_schema(), + ) + + def _get_options_schema(self): + return vol.Schema( + { + vol.Optional( + CONF_MODE, + default=self.config_entry.options.get( + CONF_MODE, DEFAULT_FORECAST_MODE + ), + ): vol.In(FORECAST_MODES), + vol.Optional( + CONF_LANGUAGE, + default=self.config_entry.options.get( + CONF_LANGUAGE, DEFAULT_LANGUAGE + ), + ): vol.In(LANGUAGES), + } + ) + + +async def _is_owm_api_online(hass, api_key): + owm = OWM(api_key) + return await hass.async_add_executor_job(owm.is_API_online) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py new file mode 100644 index 00000000000..f2507527499 --- /dev/null +++ b/homeassistant/components/openweathermap/const.py @@ -0,0 +1,142 @@ +"""Consts for the OpenWeatherMap.""" +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +) +from homeassistant.const import ( + DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + PRESSURE_PA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, + UNIT_PERCENTAGE, +) + +DOMAIN = "openweathermap" +DEFAULT_NAME = "OpenWeatherMap" +DEFAULT_LANGUAGE = "en" +DEFAULT_FORECAST_MODE = "freedaily" +ATTRIBUTION = "Data provided by OpenWeatherMap" +CONF_LANGUAGE = "language" +ENTRY_NAME = "name" +ENTRY_FORECAST_COORDINATOR = "forecast_coordinator" +ENTRY_WEATHER_COORDINATOR = "weather_coordinator" +ATTR_API_PRECIPITATION = "precipitation" +ATTR_API_DATETIME = "datetime" +ATTR_API_WEATHER = "weather" +ATTR_API_TEMPERATURE = "temperature" +ATTR_API_WIND_SPEED = "wind_speed" +ATTR_API_WIND_BEARING = "wind_bearing" +ATTR_API_HUMIDITY = "humidity" +ATTR_API_PRESSURE = "pressure" +ATTR_API_CONDITION = "condition" +ATTR_API_CLOUDS = "clouds" +ATTR_API_RAIN = "rain" +ATTR_API_SNOW = "snow" +ATTR_API_WEATHER_CODE = "weather_code" +ATTR_API_FORECAST = "forecast" +ATTR_API_THIS_DAY_FORECAST = "this_day_forecast" +SENSOR_NAME = "sensor_name" +SENSOR_UNIT = "sensor_unit" +SENSOR_DEVICE_CLASS = "sensor_device_class" +UPDATE_LISTENER = "update_listener" +COMPONENTS = ["sensor", "weather"] +FORECAST_MODES = ["hourly", "daily", "freedaily"] +MONITORED_CONDITIONS = [ + ATTR_API_WEATHER, + ATTR_API_TEMPERATURE, + ATTR_API_WIND_SPEED, + ATTR_API_WIND_BEARING, + ATTR_API_HUMIDITY, + ATTR_API_PRESSURE, + ATTR_API_CLOUDS, + ATTR_API_RAIN, + ATTR_API_SNOW, + ATTR_API_CONDITION, + ATTR_API_WEATHER_CODE, +] +FORECAST_MONITORED_CONDITIONS = [ + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +] +LANGUAGES = ["en", "es", "ru", "it"] +CONDITION_CLASSES = { + "cloudy": [803, 804], + "fog": [701, 741], + "hail": [906], + "lightning": [210, 211, 212, 221], + "lightning-rainy": [200, 201, 202, 230, 231, 232], + "partlycloudy": [801, 802], + "pouring": [504, 314, 502, 503, 522], + "rainy": [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521], + "snowy": [600, 601, 602, 611, 612, 620, 621, 622], + "snowy-rainy": [511, 615, 616], + "sunny": [800], + "windy": [905, 951, 952, 953, 954, 955, 956, 957], + "windy-variant": [958, 959, 960, 961], + "exceptional": [711, 721, 731, 751, 761, 762, 771, 900, 901, 962, 903, 904], +} +WEATHER_SENSOR_TYPES = { + ATTR_API_WEATHER: {SENSOR_NAME: "Weather"}, + ATTR_API_TEMPERATURE: { + SENSOR_NAME: "Temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_API_WIND_SPEED: { + SENSOR_NAME: "Wind speed", + SENSOR_UNIT: SPEED_METERS_PER_SECOND, + }, + ATTR_API_WIND_BEARING: {SENSOR_NAME: "Wind bearing", SENSOR_UNIT: DEGREE}, + ATTR_API_HUMIDITY: { + SENSOR_NAME: "Humidity", + SENSOR_UNIT: UNIT_PERCENTAGE, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + }, + ATTR_API_PRESSURE: { + SENSOR_NAME: "Pressure", + SENSOR_UNIT: PRESSURE_PA, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + }, + ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: UNIT_PERCENTAGE}, + ATTR_API_RAIN: {SENSOR_NAME: "Rain", SENSOR_UNIT: "mm"}, + ATTR_API_SNOW: {SENSOR_NAME: "Snow", SENSOR_UNIT: "mm"}, + ATTR_API_CONDITION: {SENSOR_NAME: "Condition"}, + ATTR_API_WEATHER_CODE: {SENSOR_NAME: "Weather Code"}, +} +FORECAST_SENSOR_TYPES = { + ATTR_FORECAST_CONDITION: {SENSOR_NAME: "Condition"}, + ATTR_FORECAST_PRECIPITATION: {SENSOR_NAME: "Precipitation"}, + ATTR_FORECAST_TEMP: { + SENSOR_NAME: "Temperature", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_FORECAST_TEMP_LOW: { + SENSOR_NAME: "Temperature Low", + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + }, + ATTR_FORECAST_TIME: { + SENSOR_NAME: "Time", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, + ATTR_API_WIND_BEARING: {SENSOR_NAME: "Wind bearing", SENSOR_UNIT: DEGREE}, + ATTR_API_WIND_SPEED: { + SENSOR_NAME: "Wind speed", + SENSOR_UNIT: SPEED_METERS_PER_SECOND, + }, +} diff --git a/homeassistant/components/openweathermap/forecast_update_coordinator.py b/homeassistant/components/openweathermap/forecast_update_coordinator.py new file mode 100644 index 00000000000..66fa7d39ab4 --- /dev/null +++ b/homeassistant/components/openweathermap/forecast_update_coordinator.py @@ -0,0 +1,137 @@ +"""Forecast data coordinator for the OpenWeatherMap (OWM) service.""" +from datetime import timedelta +import logging + +import async_timeout +from pyowm.exceptions.api_call_error import APICallError +from pyowm.exceptions.api_response_error import UnauthorizedError + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_API_FORECAST, + ATTR_API_THIS_DAY_FORECAST, + CONDITION_CLASSES, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +FORECAST_UPDATE_INTERVAL = timedelta(minutes=30) + + +class ForecastUpdateCoordinator(DataUpdateCoordinator): + """Forecast data update coordinator.""" + + def __init__(self, owm, latitude, longitude, forecast_mode, hass): + """Initialize coordinator.""" + self._owm_client = owm + self._forecast_mode = forecast_mode + self._latitude = latitude + self._longitude = longitude + self._forecast_limit = 15 + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=FORECAST_UPDATE_INTERVAL + ) + + async def _async_update_data(self): + data = {} + with async_timeout.timeout(20): + try: + forecast_response = await self._get_owm_forecast() + data = self._convert_forecast_response(forecast_response) + except (APICallError, UnauthorizedError) as error: + raise UpdateFailed(error) from error + + return data + + async def _get_owm_forecast(self): + if self._forecast_mode == "daily": + forecast_response = await self.hass.async_add_executor_job( + self._owm_client.daily_forecast_at_coords, + self._latitude, + self._longitude, + self._forecast_limit, + ) + else: + forecast_response = await self.hass.async_add_executor_job( + self._owm_client.three_hours_forecast_at_coords, + self._latitude, + self._longitude, + ) + return forecast_response.get_forecast() + + def _convert_forecast_response(self, forecast_response): + weathers = self._get_weathers(forecast_response) + + forecast_entries = self._convert_forecast_entries(weathers) + + return { + ATTR_API_FORECAST: forecast_entries, + ATTR_API_THIS_DAY_FORECAST: forecast_entries[0], + } + + def _get_weathers(self, forecast_response): + if self._forecast_mode == "freedaily": + return forecast_response.get_weathers()[::8] + return forecast_response.get_weathers() + + def _convert_forecast_entries(self, entries): + if self._forecast_mode == "daily": + return list(map(self._convert_daily_forecast, entries)) + return list(map(self._convert_forecast, entries)) + + def _convert_daily_forecast(self, entry): + return { + ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000, + ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get("day"), + ATTR_FORECAST_TEMP_LOW: entry.get_temperature("celsius").get("night"), + ATTR_FORECAST_PRECIPITATION: self._calc_daily_precipitation( + entry.get_rain().get("all"), entry.get_snow().get("all") + ), + ATTR_FORECAST_WIND_SPEED: entry.get_wind().get("speed"), + ATTR_FORECAST_WIND_BEARING: entry.get_wind().get("deg"), + ATTR_FORECAST_CONDITION: self._get_condition(entry.get_weather_code()), + } + + def _convert_forecast(self, entry): + return { + ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000, + ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get("temp"), + ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(entry), + ATTR_FORECAST_WIND_SPEED: entry.get_wind().get("speed"), + ATTR_FORECAST_WIND_BEARING: entry.get_wind().get("deg"), + ATTR_FORECAST_CONDITION: self._get_condition(entry.get_weather_code()), + } + + @staticmethod + def _calc_daily_precipitation(rain, snow): + """Calculate the precipitation.""" + rain_value = 0 if rain is None else rain + snow_value = 0 if snow is None else snow + if round(rain_value + snow_value, 1) == 0: + return None + return round(rain_value + snow_value, 1) + + @staticmethod + def _calc_precipitation(entry): + return ( + round(entry.get_rain().get("1h"), 1) + if entry.get_rain().get("1h") is not None + and (round(entry.get_rain().get("1h"), 1) > 0) + else None + ) + + @staticmethod + def _get_condition(weather_code): + return [k for k, v in CONDITION_CLASSES.items() if weather_code in v][0] diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index eafbfbe243c..dcd5d15f18d 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -1,7 +1,8 @@ { "domain": "openweathermap", - "name": "Openweathermap", + "name": "OpenWeatherMap", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openweathermap", "requirements": ["pyowm==2.10.0"], - "codeowners": ["@fabaff"] + "codeowners": ["@fabaff", "@freekode"] } diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 938fb609158..c6363107d45 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -1,227 +1,105 @@ """Support for the OpenWeatherMap (OWM) service.""" -from datetime import timedelta import logging -from pyowm import OWM -from pyowm.exceptions.api_call_error import APICallError -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - DEGREE, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, +from .abstract_owm_sensor import AbstractOpenWeatherMapSensor +from .const import ( + ATTR_API_THIS_DAY_FORECAST, + DOMAIN, + ENTRY_FORECAST_COORDINATOR, + ENTRY_NAME, + ENTRY_WEATHER_COORDINATOR, + FORECAST_MONITORED_CONDITIONS, + FORECAST_SENSOR_TYPES, + MONITORED_CONDITIONS, + WEATHER_SENSOR_TYPES, ) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle +from .forecast_update_coordinator import ForecastUpdateCoordinator +from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by OpenWeatherMap" -CONF_FORECAST = "forecast" -CONF_LANGUAGE = "language" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up OpenWeatherMap sensor entities based on a config entry.""" + domain_data = hass.data[DOMAIN][config_entry.entry_id] + name = domain_data[ENTRY_NAME] + weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] + forecast_coordinator = domain_data[ENTRY_FORECAST_COORDINATOR] -DEFAULT_NAME = "OWM" + weather_sensor_types = WEATHER_SENSOR_TYPES + forecast_sensor_types = FORECAST_SENSOR_TYPES -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) - -SENSOR_TYPES = { - "weather": ["Condition", None], - "temperature": ["Temperature", None], - "wind_speed": ["Wind speed", SPEED_METERS_PER_SECOND], - "wind_bearing": ["Wind bearing", DEGREE], - "humidity": ["Humidity", UNIT_PERCENTAGE], - "pressure": ["Pressure", "mbar"], - "clouds": ["Cloud coverage", UNIT_PERCENTAGE], - "rain": ["Rain", "mm"], - "snow": ["Snow", "mm"], - "weather_code": ["Weather code", None], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_FORECAST, default=False): cv.boolean, - vol.Optional(CONF_LANGUAGE): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the OpenWeatherMap sensor.""" - - if None in (hass.config.latitude, hass.config.longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return - - SENSOR_TYPES["temperature"][1] = hass.config.units.temperature_unit - - name = config.get(CONF_NAME) - forecast = config.get(CONF_FORECAST) - language = config.get(CONF_LANGUAGE) - if isinstance(language, str): - language = language.lower()[:2] - - owm = OWM(API_key=config.get(CONF_API_KEY), language=language) - - if not owm: - _LOGGER.error("Unable to connect to OpenWeatherMap") - return - - data = WeatherData(owm, forecast, hass.config.latitude, hass.config.longitude) - dev = [] - for variable in config[CONF_MONITORED_CONDITIONS]: - dev.append( - OpenWeatherMapSensor(name, data, variable, SENSOR_TYPES[variable][1]) + entities = [] + for sensor_type in MONITORED_CONDITIONS: + unique_id = f"{config_entry.unique_id}-{sensor_type}" + entities.append( + OpenWeatherMapSensor( + name, + unique_id, + sensor_type, + weather_sensor_types[sensor_type], + weather_coordinator, + ) ) - if forecast: - SENSOR_TYPES["forecast"] = ["Forecast", None] - dev.append( - OpenWeatherMapSensor(name, data, "forecast", SENSOR_TYPES["temperature"][1]) + for sensor_type in FORECAST_MONITORED_CONDITIONS: + unique_id = f"{config_entry.unique_id}-forecast-{sensor_type}" + entities.append( + OpenWeatherMapForecastSensor( + f"{name} Forecast", + unique_id, + sensor_type, + forecast_sensor_types[sensor_type], + forecast_coordinator, + ) ) - add_entities(dev, True) + async_add_entities(entities) -class OpenWeatherMapSensor(Entity): +class OpenWeatherMapSensor(AbstractOpenWeatherMapSensor): """Implementation of an OpenWeatherMap sensor.""" - def __init__(self, name, weather_data, sensor_type, temp_unit): + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + weather_coordinator: WeatherUpdateCoordinator, + ): """Initialize the sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] - self.owa_client = weather_data - self.temp_unit = temp_unit - self.type = sensor_type - self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" + super().__init__( + name, unique_id, sensor_type, sensor_configuration, weather_coordinator + ) + self._weather_coordinator = weather_coordinator @property def state(self): """Return the state of the device.""" - return self._state + return self._weather_coordinator.data.get(self._sensor_type, None) + + +class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): + """Implementation of an OpenWeatherMap this day forecast sensor.""" + + def __init__( + self, + name, + unique_id, + sensor_type, + sensor_configuration, + forecast_coordinator: ForecastUpdateCoordinator, + ): + """Initialize the sensor.""" + super().__init__( + name, unique_id, sensor_type, sensor_configuration, forecast_coordinator + ) + self._forecast_coordinator = forecast_coordinator @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - def update(self): - """Get the latest data from OWM and updates the states.""" - try: - self.owa_client.update() - except APICallError: - _LOGGER.error("Error when calling API to update data") - return - - data = self.owa_client.data - fc_data = self.owa_client.fc_data - - if data is None: - return - - try: - if self.type == "weather": - self._state = data.get_detailed_status() - elif self.type == "temperature": - if self.temp_unit == TEMP_CELSIUS: - self._state = round(data.get_temperature("celsius")["temp"], 1) - elif self.temp_unit == TEMP_FAHRENHEIT: - self._state = round(data.get_temperature("fahrenheit")["temp"], 1) - else: - self._state = round(data.get_temperature()["temp"], 1) - elif self.type == "wind_speed": - self._state = round(data.get_wind()["speed"], 1) - elif self.type == "wind_bearing": - self._state = round(data.get_wind()["deg"], 1) - elif self.type == "humidity": - self._state = round(data.get_humidity(), 1) - elif self.type == "pressure": - self._state = round(data.get_pressure()["press"], 0) - elif self.type == "clouds": - self._state = data.get_clouds() - elif self.type == "rain": - rain = data.get_rain() - if "1h" in rain: - self._state = round(rain["1h"], 0) - self._unit_of_measurement = "mm" - else: - self._state = "not raining" - self._unit_of_measurement = "" - elif self.type == "snow": - snow = data.get_snow() - if "1h" in snow: - self._state = round(snow["1h"], 0) - self._unit_of_measurement = "mm" - else: - self._state = "not snowing" - self._unit_of_measurement = "" - elif self.type == "forecast": - if fc_data is None: - return - self._state = fc_data.get_weathers()[0].get_detailed_status() - elif self.type == "weather_code": - self._state = data.get_weather_code() - except KeyError: - self._state = None - _LOGGER.warning("Condition is currently not available: %s", self.type) - - -class WeatherData: - """Get the latest data from OpenWeatherMap.""" - - def __init__(self, owm, forecast, latitude, longitude): - """Initialize the data object.""" - self.owm = owm - self.forecast = forecast - self.latitude = latitude - self.longitude = longitude - self.data = None - self.fc_data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from OpenWeatherMap.""" - try: - obs = self.owm.weather_at_coords(self.latitude, self.longitude) - except (APICallError, TypeError): - _LOGGER.error("Error when calling API to get weather at coordinates") - obs = None - - if obs is None: - _LOGGER.warning("Failed to fetch data") - return - - self.data = obs.get_weather() - - if self.forecast == 1: - try: - obs = self.owm.three_hours_forecast_at_coords( - self.latitude, self.longitude - ) - self.fc_data = obs.get_forecast() - except (ConnectionResetError, TypeError): - _LOGGER.warning("Failed to fetch forecast") + def state(self): + """Return the state of the device.""" + return self._forecast_coordinator.data[ATTR_API_THIS_DAY_FORECAST].get( + self._sensor_type, None + ) diff --git a/homeassistant/components/openweathermap/strings.json b/homeassistant/components/openweathermap/strings.json new file mode 100644 index 00000000000..e068bb91964 --- /dev/null +++ b/homeassistant/components/openweathermap/strings.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "OpenWeatherMap integration for these coordinates is already configured." + }, + "error": { + "auth": "API key is not correct.", + "connection": "Can't connect to OWM API" + }, + "step": { + "user": { + "data": { + "api_key": "OpenWeatherMap API key", + "language": "Language", + "latitude": "Latitude", + "longitude": "Longitude", + "mode": "Mode", + "name": "Name of the integration" + }, + "description": "Set up OpenWeatherMap integration. To generate API key go to https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Language", + "mode": "Mode" + } + } + } + } +} diff --git a/homeassistant/components/openweathermap/translations/en.json b/homeassistant/components/openweathermap/translations/en.json new file mode 100644 index 00000000000..e068bb91964 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/en.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "OpenWeatherMap integration for these coordinates is already configured." + }, + "error": { + "auth": "API key is not correct.", + "connection": "Can't connect to OWM API" + }, + "step": { + "user": { + "data": { + "api_key": "OpenWeatherMap API key", + "language": "Language", + "latitude": "Latitude", + "longitude": "Longitude", + "mode": "Mode", + "name": "Name of the integration" + }, + "description": "Set up OpenWeatherMap integration. To generate API key go to https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Language", + "mode": "Mode" + } + } + } + } +} diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 7200e759d3b..cde7eb96732 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,131 +1,89 @@ """Support for the OpenWeatherMap (OWM) service.""" -from datetime import timedelta import logging -from pyowm import OWM -from pyowm.exceptions.api_call_error import APICallError -import voluptuous as vol +from homeassistant.components.weather import WeatherEntity +from homeassistant.const import TEMP_CELSIUS -from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, - PLATFORM_SCHEMA, - WeatherEntity, +from .const import ( + ATTR_API_CONDITION, + ATTR_API_FORECAST, + ATTR_API_HUMIDITY, + ATTR_API_PRESSURE, + ATTR_API_TEMPERATURE, + ATTR_API_WIND_BEARING, + ATTR_API_WIND_SPEED, + ATTRIBUTION, + DOMAIN, + ENTRY_FORECAST_COORDINATOR, + ENTRY_NAME, + ENTRY_WEATHER_COORDINATOR, ) -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MODE, - CONF_NAME, - PRESSURE_HPA, - PRESSURE_INHG, - STATE_UNKNOWN, - TEMP_CELSIUS, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle -from homeassistant.util.pressure import convert as convert_pressure +from .forecast_update_coordinator import ForecastUpdateCoordinator +from .weather_update_coordinator import WeatherUpdateCoordinator _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by OpenWeatherMap" -FORECAST_MODE = ["hourly", "daily", "freedaily"] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up OpenWeatherMap weather entity based on a config entry.""" + domain_data = hass.data[DOMAIN][config_entry.entry_id] + name = domain_data[ENTRY_NAME] + weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] + forecast_coordinator = domain_data[ENTRY_FORECAST_COORDINATOR] -DEFAULT_NAME = "OpenWeatherMap" - -MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) - -CONDITION_CLASSES = { - "cloudy": [803, 804], - "fog": [701, 741], - "hail": [906], - "lightning": [210, 211, 212, 221], - "lightning-rainy": [200, 201, 202, 230, 231, 232], - "partlycloudy": [801, 802], - "pouring": [504, 314, 502, 503, 522], - "rainy": [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521], - "snowy": [600, 601, 602, 611, 612, 620, 621, 622], - "snowy-rainy": [511, 615, 616], - "sunny": [800], - "windy": [905, 951, 952, 953, 954, 955, 956, 957], - "windy-variant": [958, 959, 960, 961], - "exceptional": [711, 721, 731, 751, 761, 762, 771, 900, 901, 962, 903, 904], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_MODE, default="hourly"): vol.In(FORECAST_MODE), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the OpenWeatherMap weather platform.""" - - longitude = config.get(CONF_LONGITUDE, round(hass.config.longitude, 5)) - latitude = config.get(CONF_LATITUDE, round(hass.config.latitude, 5)) - name = config.get(CONF_NAME) - mode = config.get(CONF_MODE) - - try: - owm = OWM(config.get(CONF_API_KEY)) - except APICallError: - _LOGGER.error("Error while connecting to OpenWeatherMap") - return False - - data = WeatherData(owm, latitude, longitude, mode) - - add_entities( - [OpenWeatherMapWeather(name, data, hass.config.units.temperature_unit, mode)], - True, + unique_id = f"{config_entry.unique_id}" + owm_weather = OpenWeatherMapWeather( + name, unique_id, weather_coordinator, forecast_coordinator ) + async_add_entities([owm_weather], False) + class OpenWeatherMapWeather(WeatherEntity): """Implementation of an OpenWeatherMap sensor.""" - def __init__(self, name, owm, temperature_unit, mode): + def __init__( + self, + name, + unique_id, + weather_coordinator: WeatherUpdateCoordinator, + forecast_coordinator: ForecastUpdateCoordinator, + ): """Initialize the sensor.""" self._name = name - self._owm = owm - self._temperature_unit = temperature_unit - self._mode = mode - self.data = None - self.forecast_data = None + self._unique_id = unique_id + self._weather_coordinator = weather_coordinator + self._forecast_coordinator = forecast_coordinator @property def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def should_poll(self): + """Return the polling requirement of the entity.""" + return False + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + @property def condition(self): """Return the current condition.""" - try: - return [ - k - for k, v in CONDITION_CLASSES.items() - if self.data.get_weather_code() in v - ][0] - except IndexError: - return STATE_UNKNOWN + return self._weather_coordinator.data[ATTR_API_CONDITION] @property def temperature(self): """Return the temperature.""" - return self.data.get_temperature("celsius").get("temp") + return self._weather_coordinator.data[ATTR_API_TEMPERATURE] @property def temperature_unit(self): @@ -135,146 +93,49 @@ class OpenWeatherMapWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - pressure = self.data.get_pressure().get("press") - if self.hass.config.units.name == "imperial": - return round(convert_pressure(pressure, PRESSURE_HPA, PRESSURE_INHG), 2) - return pressure + return self._weather_coordinator.data[ATTR_API_PRESSURE] @property def humidity(self): """Return the humidity.""" - return self.data.get_humidity() + return self._weather_coordinator.data[ATTR_API_HUMIDITY] @property def wind_speed(self): """Return the wind speed.""" + wind_speed = self._weather_coordinator.data[ATTR_API_WIND_SPEED] if self.hass.config.units.name == "imperial": - return round(self.data.get_wind().get("speed") * 2.24, 2) - - return round(self.data.get_wind().get("speed") * 3.6, 2) + return round(wind_speed * 2.24, 2) + return round(wind_speed * 3.6, 2) @property def wind_bearing(self): """Return the wind bearing.""" - return self.data.get_wind().get("deg") - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION + return self._weather_coordinator.data[ATTR_API_WIND_BEARING] @property def forecast(self): """Return the forecast array.""" - data = [] + return self._forecast_coordinator.data[ATTR_API_FORECAST] - def calc_precipitation(rain, snow): - """Calculate the precipitation.""" - rain_value = 0 if rain is None else rain - snow_value = 0 if snow is None else snow - if round(rain_value + snow_value, 1) == 0: - return None - return round(rain_value + snow_value, 1) + @property + def available(self): + """Return True if entity is available.""" + return ( + self._weather_coordinator.last_update_success + and self._forecast_coordinator.last_update_success + ) - if self._mode == "freedaily": - weather = self.forecast_data.get_weathers()[::8] - else: - weather = self.forecast_data.get_weathers() + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self._weather_coordinator.async_add_listener(self.async_write_ha_state) + ) + self.async_on_remove( + self._forecast_coordinator.async_add_listener(self.async_write_ha_state) + ) - for entry in weather: - if self._mode == "daily": - data.append( - { - ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000, - ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get("day"), - ATTR_FORECAST_TEMP_LOW: entry.get_temperature("celsius").get( - "night" - ), - ATTR_FORECAST_PRECIPITATION: calc_precipitation( - entry.get_rain().get("all"), entry.get_snow().get("all") - ), - ATTR_FORECAST_WIND_SPEED: entry.get_wind().get("speed"), - ATTR_FORECAST_WIND_BEARING: entry.get_wind().get("deg"), - ATTR_FORECAST_CONDITION: [ - k - for k, v in CONDITION_CLASSES.items() - if entry.get_weather_code() in v - ][0], - } - ) - else: - rain = entry.get_rain().get("1h") - if rain is not None: - rain = round(rain, 1) - data.append( - { - ATTR_FORECAST_TIME: entry.get_reference_time("unix") * 1000, - ATTR_FORECAST_TEMP: entry.get_temperature("celsius").get( - "temp" - ), - ATTR_FORECAST_PRECIPITATION: rain, - ATTR_FORECAST_CONDITION: [ - k - for k, v in CONDITION_CLASSES.items() - if entry.get_weather_code() in v - ][0], - } - ) - return data - - def update(self): + async def async_update(self): """Get the latest data from OWM and updates the states.""" - try: - self._owm.update() - self._owm.update_forecast() - except APICallError: - _LOGGER.error("Exception when calling OWM web API to update data") - return - - self.data = self._owm.data - self.forecast_data = self._owm.forecast_data - - -class WeatherData: - """Get the latest data from OpenWeatherMap.""" - - def __init__(self, owm, latitude, longitude, mode): - """Initialize the data object.""" - self._mode = mode - self.owm = owm - self.latitude = latitude - self.longitude = longitude - self.data = None - self.forecast_data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from OpenWeatherMap.""" - obs = self.owm.weather_at_coords(self.latitude, self.longitude) - if obs is None: - _LOGGER.warning("Failed to fetch data from OWM") - return - - self.data = obs.get_weather() - - @Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES) - def update_forecast(self): - """Get the latest forecast from OpenWeatherMap.""" - try: - if self._mode == "daily": - fcd = self.owm.daily_forecast_at_coords( - self.latitude, self.longitude, 15 - ) - else: - fcd = self.owm.three_hours_forecast_at_coords( - self.latitude, self.longitude - ) - except APICallError: - _LOGGER.error("Exception when calling OWM web API to update forecast") - return - - if fcd is None: - _LOGGER.warning("Failed to fetch forecast data from OWM") - return - - self.forecast_data = fcd.get_forecast() + await self._weather_coordinator.async_request_refresh() + await self._forecast_coordinator.async_request_refresh() diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py new file mode 100644 index 00000000000..3c042ae1c80 --- /dev/null +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -0,0 +1,94 @@ +"""Weather data coordinator for the OpenWeatherMap (OWM) service.""" +from datetime import timedelta +import logging + +import async_timeout +from pyowm.exceptions.api_call_error import APICallError +from pyowm.exceptions.api_response_error import UnauthorizedError + +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_API_CLOUDS, + ATTR_API_CONDITION, + ATTR_API_HUMIDITY, + ATTR_API_PRESSURE, + ATTR_API_RAIN, + ATTR_API_SNOW, + ATTR_API_TEMPERATURE, + ATTR_API_WEATHER, + ATTR_API_WEATHER_CODE, + ATTR_API_WIND_BEARING, + ATTR_API_WIND_SPEED, + CONDITION_CLASSES, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +WEATHER_UPDATE_INTERVAL = timedelta(minutes=10) + + +class WeatherUpdateCoordinator(DataUpdateCoordinator): + """Weather data update coordinator.""" + + def __init__(self, owm, latitude, longitude, hass): + """Initialize coordinator.""" + self._owm_client = owm + self._latitude = latitude + self._longitude = longitude + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=WEATHER_UPDATE_INTERVAL + ) + + async def _async_update_data(self): + data = {} + with async_timeout.timeout(20): + try: + weather_response = await self._get_owm_weather() + data = self._convert_weather_response(weather_response) + except (APICallError, UnauthorizedError) as error: + raise UpdateFailed(error) from error + return data + + async def _get_owm_weather(self): + weather = await self.hass.async_add_executor_job( + self._owm_client.weather_at_coords, self._latitude, self._longitude + ) + return weather.get_weather() + + def _convert_weather_response(self, weather_response): + return { + ATTR_API_TEMPERATURE: weather_response.get_temperature("celsius").get( + "temp" + ), + ATTR_API_PRESSURE: weather_response.get_pressure().get("press"), + ATTR_API_HUMIDITY: weather_response.get_humidity(), + ATTR_API_WIND_BEARING: weather_response.get_wind().get("deg"), + ATTR_API_WIND_SPEED: weather_response.get_wind().get("speed"), + ATTR_API_CLOUDS: weather_response.get_clouds(), + ATTR_API_RAIN: self._get_rain(weather_response.get_rain()), + ATTR_API_SNOW: self._get_snow(weather_response.get_snow()), + ATTR_API_WEATHER: weather_response.get_detailed_status(), + ATTR_API_CONDITION: self._get_condition( + weather_response.get_weather_code() + ), + ATTR_API_WEATHER_CODE: weather_response.get_weather_code(), + } + + @staticmethod + def _get_rain(rain): + if "1h" in rain: + return round(rain["1h"], 0) + return "not raining" + + @staticmethod + def _get_snow(snow): + if snow: + return round(snow, 0) + return "not snowing" + + @staticmethod + def _get_condition(weather_code): + return [k for k, v in CONDITION_CLASSES.items() if weather_code in v][0] diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 05ce927c773..86d778db825 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -128,6 +128,7 @@ FLOWS = [ "onvif", "opentherm_gw", "openuv", + "openweathermap", "ovo_energy", "owntracks", "ozw", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff64f2253e0..8d03da1e001 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -756,6 +756,9 @@ pyotgw==0.6b1 # homeassistant.components.otp pyotp==2.3.0 +# homeassistant.components.openweathermap +pyowm==2.10.0 + # homeassistant.components.point pypoint==1.1.2 diff --git a/tests/components/openweathermap/__init__.py b/tests/components/openweathermap/__init__.py new file mode 100644 index 00000000000..e718962766f --- /dev/null +++ b/tests/components/openweathermap/__init__.py @@ -0,0 +1 @@ +"""Tests for the OpenWeatherMap integration.""" diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py new file mode 100644 index 00000000000..672e7358803 --- /dev/null +++ b/tests/components/openweathermap/test_config_flow.py @@ -0,0 +1,232 @@ +"""Define tests for the OpenWeatherMap config flow.""" +from asynctest import MagicMock, patch +from pyowm.exceptions.api_call_error import APICallError +from pyowm.exceptions.api_response_error import UnauthorizedError + +from homeassistant import data_entry_flow +from homeassistant.components.openweathermap.const import ( + CONF_LANGUAGE, + DEFAULT_FORECAST_MODE, + DEFAULT_LANGUAGE, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, +) + +from tests.common import MockConfigEntry + +CONFIG = { + CONF_NAME: "openweathermap", + CONF_API_KEY: "foo", + CONF_LATITUDE: 50, + CONF_LONGITUDE: 40, + CONF_MODE: DEFAULT_FORECAST_MODE, + CONF_LANGUAGE: DEFAULT_LANGUAGE, +} + +VALID_YAML_CONFIG = {CONF_API_KEY: "foo"} + + +async def test_form(hass): + """Test that the form is served with valid input.""" + mocked_owm = _create_mocked_owm(True) + + with patch( + "pyowm.weatherapi25.owm25.OWM25", + return_value=mocked_owm, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + entry = conf_entries[0] + assert entry.state == "loaded" + + await hass.config_entries.async_unload(conf_entries[0].entry_id) + await hass.async_block_till_done() + assert entry.state == "not_loaded" + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_NAME] + assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] + assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] + assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] + + +async def test_form_import(hass): + """Test we can import yaml config.""" + mocked_owm = _create_mocked_owm(True) + + with patch("pyowm.weatherapi25.owm25.OWM25", return_value=mocked_owm), patch( + "homeassistant.components.openweathermap.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.openweathermap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=VALID_YAML_CONFIG.copy(), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_LATITUDE] == hass.config.latitude + assert result["data"][CONF_LONGITUDE] == hass.config.longitude + assert result["data"][CONF_API_KEY] == VALID_YAML_CONFIG[CONF_API_KEY] + + 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_options(hass): + """Test that the options form.""" + mocked_owm = _create_mocked_owm(True) + + with patch( + "pyowm.weatherapi25.owm25.OWM25", + return_value=mocked_owm, + ): + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="openweathermap_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == "loaded" + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_MODE: "daily"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONF_MODE: "daily", + CONF_LANGUAGE: DEFAULT_LANGUAGE, + } + + await hass.async_block_till_done() + + assert config_entry.state == "loaded" + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_MODE: "freedaily"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + CONF_MODE: "freedaily", + CONF_LANGUAGE: DEFAULT_LANGUAGE, + } + + await hass.async_block_till_done() + + assert config_entry.state == "loaded" + + +async def test_form_invalid_api_key(hass): + """Test that the form is served with no input.""" + mocked_owm = _create_mocked_owm(True) + + with patch( + "pyowm.weatherapi25.owm25.OWM25", + return_value=mocked_owm, + side_effect=UnauthorizedError(""), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["errors"] == {"base": "auth"} + + +async def test_form_api_call_error(hass): + """Test setting up with api call error.""" + mocked_owm = _create_mocked_owm(True) + + with patch( + "pyowm.weatherapi25.owm25.OWM25", + return_value=mocked_owm, + side_effect=APICallError(""), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["errors"] == {"base": "connection"} + + +async def test_form_api_offline(hass): + """Test setting up with api call error.""" + mocked_owm = _create_mocked_owm(False) + + with patch( + "homeassistant.components.openweathermap.config_flow.OWM", + return_value=mocked_owm, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["errors"] == {"base": "auth"} + + +def _create_mocked_owm(is_api_online: bool): + mocked_owm = MagicMock() + mocked_owm.is_API_online.return_value = is_api_online + + weather = MagicMock() + weather.get_temperature.return_value.get.return_value = 10 + weather.get_pressure.return_value.get.return_value = 10 + weather.get_humidity.return_value = 10 + weather.get_wind.return_value.get.return_value = 0 + weather.get_clouds.return_value = "clouds" + weather.get_rain.return_value = [] + weather.get_snow.return_value = 3 + weather.get_detailed_status.return_value = "status" + weather.get_weather_code.return_value = 803 + + mocked_owm.weather_at_coords.return_value.get_weather.return_value = weather + + one_day_forecast = MagicMock() + one_day_forecast.get_reference_time.return_value = 10 + one_day_forecast.get_temperature.return_value.get.return_value = 10 + one_day_forecast.get_rain.return_value.get.return_value = 0 + one_day_forecast.get_snow.return_value.get.return_value = 0 + one_day_forecast.get_wind.return_value.get.return_value = 0 + one_day_forecast.get_weather_code.return_value = 803 + + mocked_owm.three_hours_forecast_at_coords.return_value.get_forecast.return_value.get_weathers.return_value = [ + one_day_forecast + ] + + return mocked_owm From 2183ff5906b67f3de925f53bc5ccf39495820f5e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 5 Sep 2020 00:04:17 +0000 Subject: [PATCH 658/862] [ci skip] Translation update --- homeassistant/components/dsmr/translations/no.json | 7 +++++++ homeassistant/components/dsmr/translations/ru.json | 7 +++++++ homeassistant/components/dsmr/translations/zh-Hant.json | 7 +++++++ .../components/homematicip_cloud/translations/no.json | 3 ++- .../components/homematicip_cloud/translations/ru.json | 2 +- .../components/openweathermap/translations/en.json | 2 +- homeassistant/components/xiaomi_aqara/translations/no.json | 2 +- homeassistant/components/xiaomi_aqara/translations/ru.json | 2 +- .../components/xiaomi_aqara/translations/zh-Hant.json | 2 +- 9 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/dsmr/translations/no.json create mode 100644 homeassistant/components/dsmr/translations/ru.json create mode 100644 homeassistant/components/dsmr/translations/zh-Hant.json diff --git a/homeassistant/components/dsmr/translations/no.json b/homeassistant/components/dsmr/translations/no.json new file mode 100644 index 00000000000..6ba5a1f3978 --- /dev/null +++ b/homeassistant/components/dsmr/translations/no.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/ru.json b/homeassistant/components/dsmr/translations/ru.json new file mode 100644 index 00000000000..4ad85f691be --- /dev/null +++ b/homeassistant/components/dsmr/translations/ru.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json new file mode 100644 index 00000000000..1ab3e1f720f --- /dev/null +++ b/homeassistant/components/dsmr/translations/zh-Hant.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/no.json b/homeassistant/components/homematicip_cloud/translations/no.json index 1879860f7a7..9a20243c1b9 100644 --- a/homeassistant/components/homematicip_cloud/translations/no.json +++ b/homeassistant/components/homematicip_cloud/translations/no.json @@ -6,7 +6,8 @@ "unknown": "Ukjent feil oppstod." }, "error": { - "invalid_sgtin_or_pin": "Ugyldig PIN kode, pr\u00f8v igjen.", + "invalid_pin": "Ugyldig PIN kode, pr\u00f8v igjen.", + "invalid_sgtin_or_pin": "Ugyldig SGTIN eller PIN-kode. Pr\u00f8v p\u00e5 nytt.", "press_the_button": "Vennligst trykk p\u00e5 den bl\u00e5 knappen.", "register_failed": "Kunne ikke registrere, vennligst pr\u00f8v igjen.", "timeout_button": "Bl\u00e5 knapp-trykk tok for lang tid, vennligst pr\u00f8v igjen." diff --git a/homeassistant/components/homematicip_cloud/translations/ru.json b/homeassistant/components/homematicip_cloud/translations/ru.json index df87a5e42af..b307140ef02 100644 --- a/homeassistant/components/homematicip_cloud/translations/ru.json +++ b/homeassistant/components/homematicip_cloud/translations/ru.json @@ -7,7 +7,7 @@ }, "error": { "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", - "invalid_sgtin_or_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", + "invalid_sgtin_or_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 SGTIN \u0438\u043b\u0438 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "press_the_button": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443.", "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430." diff --git a/homeassistant/components/openweathermap/translations/en.json b/homeassistant/components/openweathermap/translations/en.json index e068bb91964..d42c4479711 100644 --- a/homeassistant/components/openweathermap/translations/en.json +++ b/homeassistant/components/openweathermap/translations/en.json @@ -32,4 +32,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json index c3dc7f9ac5d..0119813e3e4 100644 --- a/homeassistant/components/xiaomi_aqara/translations/no.json +++ b/homeassistant/components/xiaomi_aqara/translations/no.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "Kunne ikke oppdage en Xiaomi Aqara Gateway, pr\u00f8v \u00e5 bruke IP-adressen til enheten som kj\u00f8rer HomeAssistant som grensesnitt", - "invalid_host": "Ugyldig IP adresse", + "invalid_host": "Ugyldig IP adresse , se https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Ugyldig nettverksgrensesnitt", "invalid_key": "Ugyldig gateway-n\u00f8kkel", "invalid_mac": "Ugyldig Mac-adresse", diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 79bea5f6e44..ddce68aa2bf 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437 Xiaomi Aqara, \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.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441. \u0421\u043f\u043e\u0441\u043e\u0431\u044b \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043e\u043f\u0438\u0441\u0430\u043d\u044b \u0437\u0434\u0435\u0441\u044c: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem.", "invalid_interface": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0430.", "invalid_mac": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 MAC-\u0430\u0434\u0440\u0435\u0441.", diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 955c5d39108..87ea0c6028a 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "\u63a2\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u8a2d\u5099\u7684 IP \u4f5c\u70ba\u4ecb\u9762", - "invalid_host": "\u7121\u6548\u7684 IP \u4f4d\u5740", + "invalid_host": "\u7121\u6548\u7684 IP \u4f4d\u5740\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548", "invalid_key": "\u7db2\u95dc\u5bc6\u9470\u7121\u6548", "invalid_mac": "\u7121\u6548\u7684 Mac \u4f4d\u5740", From 7be5ef8b6c70ae22c1aab8bd6f344f91f9425921 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sat, 5 Sep 2020 05:51:18 +0200 Subject: [PATCH 659/862] Improve systemmonitor (#36283) * Improvements for systemmonitor * Use MDI CPU icon (bit architecture determined at runtime) * Consistent usages of "percent" (ensured backwards compatibility by explicity setting the entity_id) * Default value "/" (root) for disk_* sensors to prevent runtime issues if not specified in configuration * Added CPU temperature sensor to systemmonitor * Code streamlining of CPU temperature retrieval * Corrected sensor state handling and added "available" logic * Corrected ENTITY_ID to include argument * Optimized "available" handling & CPU temperature detection * Corrected tuple for CPU temperature determination * - Do not create CPU temperature entity if no sensor data can be read - Re-use temperature sensor labels from "glances" integration * Array fix * Corrected sensor array access * Handle empty temperature sensor labels (same logic as used by "glances") * PR comments: Setting unique_ID and f-strings * Removed unused constants * Revert entity rename (wait until next release) --- .../components/systemmonitor/sensor.py | 84 ++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index bae42c2f50b..e1293866c1e 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -2,6 +2,7 @@ import logging import os import socket +import sys import psutil import voluptuous as vol @@ -15,10 +16,12 @@ from homeassistant.const import ( DATA_RATE_MEGABYTES_PER_SECOND, STATE_OFF, STATE_ON, + TEMP_CELSIUS, UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -27,6 +30,11 @@ _LOGGER = logging.getLogger(__name__) CONF_ARG = "arg" +if sys.maxsize > 2 ** 32: + CPU_ICON = "mdi:cpu-64-bit" +else: + CPU_ICON = "mdi:cpu-32-bit" + SENSOR_TYPES = { "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None], "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None], @@ -56,8 +64,9 @@ SENSOR_TYPES = { "mdi:server-network", None, ], - "process": ["Process", " ", "mdi:memory", None], - "processor_use": ["Processor use", UNIT_PERCENTAGE, "mdi:memory", None], + "process": ["Process", " ", CPU_ICON, None], + "processor_use": ["Processor use", UNIT_PERCENTAGE, CPU_ICON, None], + "processor_temperature": ["Processor temperature", TEMP_CELSIUS, CPU_ICON, None], "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None], "swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None], "swap_use_percent": ["Swap use (percent)", UNIT_PERCENTAGE, "mdi:harddisk", None], @@ -90,13 +99,48 @@ IO_COUNTER = { IF_ADDRS_FAMILY = {"ipv4_address": socket.AF_INET, "ipv6_address": socket.AF_INET6} +# There might be additional keys to be added for different +# platforms / hardware combinations. +# Taken from last version of "glances" integration before they moved to +# a generic temperature sensor logic. +# https://github.com/home-assistant/core/blob/5e15675593ba94a2c11f9f929cdad317e27ce190/homeassistant/components/glances/sensor.py#L199 +CPU_SENSOR_PREFIXES = [ + "amdgpu 1", + "aml_thermal", + "Core 0", + "Core 1", + "CPU Temperature", + "CPU", + "cpu-thermal 1", + "cpu_thermal 1", + "exynos-therm 1", + "Package id 0", + "Physical id 0", + "radeon 1", + "soc-thermal 1", + "soc_thermal 1", +] + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the system monitor sensors.""" dev = [] for resource in config[CONF_RESOURCES]: + # Initialize the sensor argument if none was provided. + # For disk monitoring default to "/" (root) to prevent runtime errors, if argument was not specified. if CONF_ARG not in resource: - resource[CONF_ARG] = "" + if resource[CONF_TYPE].startswith("disk_"): + resource[CONF_ARG] = "/" + else: + resource[CONF_ARG] = "" + + # Verify if we can retrieve CPU / processor temperatures. + # If not, do not create the entity and add a warning to the log + if resource[CONF_TYPE] == "processor_temperature": + if SystemMonitorSensor.read_cpu_temperature() is None: + _LOGGER.warning("Cannot read CPU / processor temperature information.") + continue + dev.append(SystemMonitorSensor(resource[CONF_TYPE], resource[CONF_ARG])) add_entities(dev, True) @@ -108,10 +152,12 @@ class SystemMonitorSensor(Entity): def __init__(self, sensor_type, argument=""): """Initialize the sensor.""" self._name = "{} {}".format(SENSOR_TYPES[sensor_type][0], argument) + self._unique_id = slugify(f"{sensor_type}_{argument}") self.argument = argument self.type = sensor_type self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._available = True if sensor_type in ["throughput_network_out", "throughput_network_in"]: self._last_value = None self._last_update_time = None @@ -121,6 +167,11 @@ class SystemMonitorSensor(Entity): """Return the name of the sensor.""" return self._name.rstrip() + @property + def unique_id(self): + """Return the unique ID.""" + return self._unique_id + @property def device_class(self): """Return the class of this sensor.""" @@ -141,6 +192,11 @@ class SystemMonitorSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + @property + def available(self): + """Return True if entity is available.""" + return self._available + def update(self): """Get the latest system information.""" if self.type == "disk_use_percent": @@ -166,6 +222,8 @@ class SystemMonitorSensor(Entity): self._state = round(psutil.swap_memory().free / 1024 ** 2, 1) elif self.type == "processor_use": self._state = round(psutil.cpu_percent(interval=None)) + elif self.type == "processor_temperature": + self._state = self.read_cpu_temperature() elif self.type == "process": for proc in psutil.process_iter(): try: @@ -231,3 +289,23 @@ class SystemMonitorSensor(Entity): self._state = round(os.getloadavg()[1], 2) elif self.type == "load_15m": self._state = round(os.getloadavg()[2], 2) + + @staticmethod + def read_cpu_temperature(): + """Attempt to read CPU / processor temperature.""" + temps = psutil.sensors_temperatures() + + for name, entries in temps.items(): + i = 1 + for entry in entries: + # In case the label is empty (e.g. on Raspberry PI 4), + # construct it ourself here based on the sensor key name. + if not entry.label: + _label = f"{name} {i}" + else: + _label = entry.label + + if _label in CPU_SENSOR_PREFIXES: + return round(entry.current, 1) + + i += 1 From 3565fec0051f78309ee1398987a0998aea6c62e7 Mon Sep 17 00:00:00 2001 From: Nishchith Shetty Date: Sat, 5 Sep 2020 14:14:30 +0530 Subject: [PATCH 660/862] Add parameters for enabling Auth in Apache Kafka integration (#39611) --- .../components/apache_kafka/__init__.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index c64e2159977..52f57a4b753 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -8,7 +8,9 @@ import voluptuous as vol from homeassistant.const import ( CONF_IP_ADDRESS, + CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE, @@ -16,6 +18,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA +from homeassistant.util import ssl as ssl_util _LOGGER = logging.getLogger(__name__) @@ -23,6 +26,7 @@ DOMAIN = "apache_kafka" CONF_FILTER = "filter" CONF_TOPIC = "topic" +CONF_SECURITY_PROTOCOL = "security_protocol" CONFIG_SCHEMA = vol.Schema( { @@ -32,6 +36,11 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_PORT): cv.port, vol.Required(CONF_TOPIC): cv.string, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + vol.Optional(CONF_SECURITY_PROTOCOL, default="PLAINTEXT"): vol.In( + ["PLAINTEXT", "SASL_SSL"] + ), + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, } ) }, @@ -49,6 +58,9 @@ async def async_setup(hass, config): conf[CONF_PORT], conf[CONF_TOPIC], conf[CONF_FILTER], + conf[CONF_SECURITY_PROTOCOL], + conf.get(CONF_USERNAME), + conf.get(CONF_PASSWORD), ) hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, kafka.shutdown()) @@ -74,15 +86,31 @@ class DateTimeJSONEncoder(json.JSONEncoder): class KafkaManager: """Define a manager to buffer events to Kafka.""" - def __init__(self, hass, ip_address, port, topic, entities_filter): + def __init__( + self, + hass, + ip_address, + port, + topic, + entities_filter, + security_protocol, + username, + password, + ): """Initialize.""" self._encoder = DateTimeJSONEncoder() self._entities_filter = entities_filter self._hass = hass + ssl_context = ssl_util.client_context() self._producer = AIOKafkaProducer( loop=hass.loop, bootstrap_servers=f"{ip_address}:{port}", compression_type="gzip", + security_protocol=security_protocol, + ssl_context=ssl_context, + sasl_mechanism="PLAIN", + sasl_plain_username=username, + sasl_plain_password=password, ) self._topic = topic From 8567fe94e1838f4a5c04f564e296af8f4b773eac Mon Sep 17 00:00:00 2001 From: Rob Bierbooms Date: Sat, 5 Sep 2020 12:05:46 +0200 Subject: [PATCH 661/862] Add connection validation on import for dsmr integration (#39664) --- homeassistant/components/dsmr/config_flow.py | 125 ++++++++++++++- homeassistant/components/dsmr/const.py | 3 + tests/components/dsmr/test_config_flow.py | 158 ++++++++++++++++++- tests/components/dsmr/test_sensor.py | 7 +- 4 files changed, 283 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index d3aa770ff60..d0d0304a02a 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -1,15 +1,114 @@ """Config flow for DSMR integration.""" +import asyncio +from functools import partial import logging from typing import Any, Dict, Optional -from homeassistant import config_entries +from async_timeout import timeout +from dsmr_parser import obis_references as obis_ref +from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +import serial + +from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_PORT -from .const import DOMAIN # pylint:disable=unused-import +from .const import ( # pylint:disable=unused-import + CONF_DSMR_VERSION, + CONF_SERIAL_ID, + CONF_SERIAL_ID_GAS, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) +class DSMRConnection: + """Test the connection to DSMR and receive telegram to read serial ids.""" + + def __init__(self, host, port, dsmr_version): + """Initialize.""" + self._host = host + self._port = port + self._dsmr_version = dsmr_version + self._telegram = {} + + def equipment_identifier(self): + """Equipment identifier.""" + if obis_ref.EQUIPMENT_IDENTIFIER in self._telegram: + dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER] + return getattr(dsmr_object, "value", None) + + def equipment_identifier_gas(self): + """Equipment identifier gas.""" + if obis_ref.EQUIPMENT_IDENTIFIER_GAS in self._telegram: + dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER_GAS] + return getattr(dsmr_object, "value", None) + + async def validate_connect(self, hass: core.HomeAssistant) -> bool: + """Test if we can validate connection with the device.""" + + def update_telegram(telegram): + self._telegram = telegram + + transport.close() + + if self._host is None: + reader_factory = partial( + create_dsmr_reader, + self._port, + self._dsmr_version, + update_telegram, + loop=hass.loop, + ) + else: + reader_factory = partial( + create_tcp_dsmr_reader, + self._host, + self._port, + self._dsmr_version, + update_telegram, + loop=hass.loop, + ) + + try: + transport, protocol = await asyncio.create_task(reader_factory()) + except (serial.serialutil.SerialException, OSError): + _LOGGER.exception("Error connecting to DSMR") + return False + + if transport: + try: + async with timeout(30): + await protocol.wait_closed() + except asyncio.TimeoutError: + # Timeout (no data received), close transport and return True (if telegram is empty, will result in CannotCommunicate error) + transport.close() + await protocol.wait_closed() + return True + + +async def _validate_dsmr_connection(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + conn = DSMRConnection(data.get(CONF_HOST), data[CONF_PORT], data[CONF_DSMR_VERSION]) + + if not await conn.validate_connect(hass): + raise CannotConnect + + equipment_identifier = conn.equipment_identifier() + equipment_identifier_gas = conn.equipment_identifier_gas() + + # Check only for equipment identifier in case no gas meter is connected + if equipment_identifier is None: + raise CannotCommunicate + + info = { + CONF_SERIAL_ID: equipment_identifier, + CONF_SERIAL_ID_GAS: equipment_identifier_gas, + } + + return info + + class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for DSMR.""" @@ -55,9 +154,29 @@ class DSMRFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if status is not None: return status + try: + info = await _validate_dsmr_connection(self.hass, import_config) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + except CannotCommunicate: + return self.async_abort(reason="cannot_communicate") + if host is not None: name = f"{host}:{port}" else: name = port - return self.async_create_entry(title=name, data=import_config) + data = {**import_config, **info} + + await self.async_set_unique_id(info[CONF_SERIAL_ID]) + self._abort_if_unique_id_configured(data) + + return self.async_create_entry(title=name, data=data) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class CannotCommunicate(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 110e6b46a99..ed5f8bf0ed7 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -8,6 +8,9 @@ CONF_DSMR_VERSION = "dsmr_version" CONF_RECONNECT_INTERVAL = "reconnect_interval" CONF_PRECISION = "precision" +CONF_SERIAL_ID = "serial_id" +CONF_SERIAL_ID_GAS = "serial_id_gas" + DEFAULT_DSMR_VERSION = "2.2" DEFAULT_PORT = "/dev/ttyUSB0" DEFAULT_PRECISION = 3 diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 1d25d2cd915..c35562b4024 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -1,12 +1,65 @@ """Test the DSMR config flow.""" +import asyncio +from itertools import chain, repeat + +from dsmr_parser.clients.protocol import DSMRProtocol +from dsmr_parser.obis_references import EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS +from dsmr_parser.objects import CosemObject +import pytest +import serial + from homeassistant import config_entries, setup from homeassistant.components.dsmr import DOMAIN -from tests.async_mock import patch +from tests.async_mock import DEFAULT, AsyncMock, Mock, patch from tests.common import MockConfigEntry +SERIAL_DATA = {"serial_id": "12345678", "serial_id_gas": "123456789"} -async def test_import_usb(hass): + +@pytest.fixture +def mock_connection_factory(monkeypatch): + """Mock the create functions for serial and TCP Asyncio connections.""" + transport = Mock(spec=asyncio.Transport) + protocol = Mock(spec=DSMRProtocol) + + async def connection_factory(*args, **kwargs): + """Return mocked out Asyncio classes.""" + return (transport, protocol) + + connection_factory = Mock(wraps=connection_factory) + + # apply the mock to both connection factories + monkeypatch.setattr( + "homeassistant.components.dsmr.config_flow.create_dsmr_reader", + connection_factory, + ) + monkeypatch.setattr( + "homeassistant.components.dsmr.config_flow.create_tcp_dsmr_reader", + connection_factory, + ) + + protocol.telegram = { + EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]), + EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]), + } + + async def wait_closed(): + if isinstance(connection_factory.call_args_list[0][0][2], str): + # TCP + telegram_callback = connection_factory.call_args_list[0][0][3] + else: + # Serial + telegram_callback = connection_factory.call_args_list[0][0][2] + + telegram_callback(protocol.telegram) + + protocol.wait_closed = wait_closed + + return connection_factory, transport, protocol + + +async def test_import_usb(hass, mock_connection_factory): """Test we can import.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -26,10 +79,103 @@ async def test_import_usb(hass): assert result["type"] == "create_entry" assert result["title"] == "/dev/ttyUSB0" - assert result["data"] == entry_data + assert result["data"] == {**entry_data, **SERIAL_DATA} -async def test_import_network(hass): +async def test_import_usb_failed_connection(hass, monkeypatch, mock_connection_factory): + """Test we can import.""" + (connection_factory, transport, protocol) = mock_connection_factory + + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + # override the mock to have it fail the first time and succeed after + first_fail_connection_factory = AsyncMock( + return_value=(transport, protocol), + side_effect=chain([serial.serialutil.SerialException], repeat(DEFAULT)), + ) + + monkeypatch.setattr( + "homeassistant.components.dsmr.config_flow.create_dsmr_reader", + first_fail_connection_factory, + ) + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +async def test_import_usb_no_data(hass, monkeypatch, mock_connection_factory): + """Test we can import.""" + (connection_factory, transport, protocol) = mock_connection_factory + + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + # override the mock to have it fail the first time and succeed after + wait_closed = AsyncMock( + return_value=(transport, protocol), + side_effect=chain([asyncio.TimeoutError], repeat(DEFAULT)), + ) + + protocol.wait_closed = wait_closed + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_communicate" + + +async def test_import_usb_wrong_telegram(hass, mock_connection_factory): + """Test we can import.""" + (connection_factory, transport, protocol) = mock_connection_factory + + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "2.2", + "precision": 4, + "reconnect_interval": 30, + } + + protocol.telegram = {} + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_communicate" + + +async def test_import_network(hass, mock_connection_factory): """Test we can import from network.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -50,10 +196,10 @@ async def test_import_network(hass): assert result["type"] == "create_entry" assert result["title"] == "localhost:1234" - assert result["data"] == entry_data + assert result["data"] == {**entry_data, **SERIAL_DATA} -async def test_import_update(hass): +async def test_import_update(hass, mock_connection_factory): """Test we can import.""" await setup.async_setup_component(hass, "persistent_notification", {}) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 73c11579070..f0ff2f85c57 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -61,8 +61,13 @@ async def test_setup_platform(hass, mock_connection_factory): "reconnect_interval": 30, } + serial_data = {"serial_id": "1234", "serial_id_gas": "5678"} + with patch("homeassistant.components.dsmr.async_setup", return_value=True), patch( "homeassistant.components.dsmr.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.dsmr.config_flow._validate_dsmr_connection", + return_value=serial_data, ): assert await async_setup_component( hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: entry_data} @@ -79,7 +84,7 @@ async def test_setup_platform(hass, mock_connection_factory): entry = conf_entries[0] assert entry.state == "loaded" - assert entry.data == entry_data + assert entry.data == {**entry_data, **serial_data} async def test_default_setup(hass, mock_connection_factory): From 3022fc4702cde93b9f2e981aa5dc0d106dbc71ce Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Sat, 5 Sep 2020 08:57:45 -0400 Subject: [PATCH 662/862] Add Emulated Kasa Integration (#39630) Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + .../components/emulated_kasa/__init__.py | 150 ++++++ .../components/emulated_kasa/const.py | 5 + .../components/emulated_kasa/manifest.json | 8 + homeassistant/components/sense/manifest.json | 2 +- requirements_all.txt | 3 +- requirements_test_all.txt | 3 +- tests/components/emulated_kasa/__init__.py | 1 + tests/components/emulated_kasa/test_init.py | 495 ++++++++++++++++++ 9 files changed, 665 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/emulated_kasa/__init__.py create mode 100644 homeassistant/components/emulated_kasa/const.py create mode 100644 homeassistant/components/emulated_kasa/manifest.json create mode 100644 tests/components/emulated_kasa/__init__.py create mode 100644 tests/components/emulated_kasa/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index e3b0f0462c3..0f48fcc3dcc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -120,6 +120,7 @@ homeassistant/components/elkm1/* @gwww @bdraco homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/emoncms/* @borpin +homeassistant/components/emulated_kasa/* @kbickar homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer homeassistant/components/entur_public_transport/* @hfurubotten diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py new file mode 100644 index 00000000000..b9dc79e25cc --- /dev/null +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -0,0 +1,150 @@ +"""Support for local power state reporting of entities by emulating TP-Link Kasa smart plugs.""" +import logging + +from sense_energy import PlugInstance, SenseLink +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import ATTR_CURRENT_POWER_W +from homeassistant.const import ( + CONF_ENTITIES, + CONF_NAME, + CONF_UNIQUE_ID, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + STATE_ON, +) +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers.template import Template, is_template_string + +from .const import CONF_POWER, CONF_POWER_ENTITY, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CONFIG_ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_POWER): vol.Any( + vol.Coerce(float), + cv.template, + ), + vol.Optional(CONF_POWER_ENTITY): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_ENTITIES): vol.Schema( + {cv.entity_id: CONFIG_ENTITY_SCHEMA} + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the emulated_kasa component.""" + conf = config.get(DOMAIN) + if not conf: + return True + entity_configs = conf[CONF_ENTITIES] + + def devices(): + """Devices to be emulated.""" + yield from get_plug_devices(hass, entity_configs) + + server = SenseLink(devices) + + async def stop_emulated_kasa(event): + await server.stop() + + async def start_emulated_kasa(event): + await validate_configs(hass, entity_configs) + try: + await server.start() + except OSError as error: + _LOGGER.error("Failed to create UDP server at port 9999: %s", error) + else: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_kasa) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_emulated_kasa) + + return True + + +async def validate_configs(hass, entity_configs): + """Validate that entities exist and ensure templates are ready to use.""" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + for entity_id, entity_config in entity_configs.items(): + state = hass.states.get(entity_id) + if state is None: + _LOGGER.debug("Entity not found: %s", entity_id) + continue + + entity = entity_registry.async_get(entity_id) + if entity: + entity_config[CONF_UNIQUE_ID] = get_system_unique_id(entity) + else: + entity_config[CONF_UNIQUE_ID] = entity_id + + if CONF_POWER in entity_config: + power_val = entity_config[CONF_POWER] + if isinstance(power_val, str) and is_template_string(power_val): + entity_config[CONF_POWER] = Template(power_val, hass) + elif isinstance(power_val, Template): + entity_config[CONF_POWER].hass = hass + elif CONF_POWER_ENTITY in entity_config: + power_val = entity_config[CONF_POWER_ENTITY] + if hass.states.get(power_val) is None: + _LOGGER.debug("Sensor Entity not found: %s", power_val) + else: + entity_config[CONF_POWER] = power_val + elif state.domain == SENSOR_DOMAIN: + pass + elif ATTR_CURRENT_POWER_W in state.attributes: + pass + else: + _LOGGER.debug("No power value defined for: %s", entity_id) + + +def get_system_unique_id(entity: RegistryEntry): + """Determine the system wide unique_id for an entity.""" + return f"{entity.platform}.{entity.domain}.{entity.unique_id}" + + +def get_plug_devices(hass, entity_configs): + """Produce list of plug devices from config entities.""" + for entity_id, entity_config in entity_configs.items(): + state = hass.states.get(entity_id) + if state is None: + continue + name = entity_config.get(CONF_NAME, state.name) + + if state.state == STATE_ON or state.domain == SENSOR_DOMAIN: + if CONF_POWER in entity_config: + power_val = entity_config[CONF_POWER] + if isinstance(power_val, (float, int)): + power = float(power_val) + elif isinstance(power_val, str): + power = float(hass.states.get(power_val).state) + elif isinstance(power_val, Template): + power = float(power_val.async_render()) + elif ATTR_CURRENT_POWER_W in state.attributes: + power = float(state.attributes[ATTR_CURRENT_POWER_W]) + elif state.domain == SENSOR_DOMAIN: + power = float(state.state) + else: + power = 0.0 + last_changed = state.last_changed.timestamp() + yield PlugInstance( + entity_config[CONF_UNIQUE_ID], + start_time=last_changed, + alias=name, + power=power, + ) diff --git a/homeassistant/components/emulated_kasa/const.py b/homeassistant/components/emulated_kasa/const.py new file mode 100644 index 00000000000..967cf90d331 --- /dev/null +++ b/homeassistant/components/emulated_kasa/const.py @@ -0,0 +1,5 @@ +"""Constants for emulated_kasa.""" + +CONF_POWER = "power" +CONF_POWER_ENTITY = "power_entity" +DOMAIN = "emulated_kasa" diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json new file mode 100644 index 00000000000..678b04bc5c0 --- /dev/null +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "emulated_kasa", + "name": "Emulated Kasa", + "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", + "requirements": ["sense_energy==0.8.0"], + "codeowners": ["@kbickar"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index deadf759f06..c0c568a8e3d 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.7.2"], + "requirements": ["sense_energy==0.8.0"], "codeowners": ["@kbickar"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 83d8503bf3f..93590f7b386 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1955,8 +1955,9 @@ sendgrid==6.4.6 # homeassistant.components.sensehat sense-hat==2.2.0 +# homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.7.2 +sense_energy==0.8.0 # homeassistant.components.sentry sentry-sdk==0.17.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d03da1e001..75249673c2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -907,8 +907,9 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws[websocket]==1.4.0 +# homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.7.2 +sense_energy==0.8.0 # homeassistant.components.sentry sentry-sdk==0.17.3 diff --git a/tests/components/emulated_kasa/__init__.py b/tests/components/emulated_kasa/__init__.py new file mode 100644 index 00000000000..a762eeca16e --- /dev/null +++ b/tests/components/emulated_kasa/__init__.py @@ -0,0 +1 @@ +"""Tests for emulated_kasa.""" diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py new file mode 100644 index 00000000000..10ccb4d68a5 --- /dev/null +++ b/tests/components/emulated_kasa/test_init.py @@ -0,0 +1,495 @@ +"""Tests for emulated_kasa library bindings.""" +import math + +from homeassistant.components import emulated_kasa +from homeassistant.components.emulated_kasa.const import ( + CONF_POWER, + CONF_POWER_ENTITY, + DOMAIN, +) +from homeassistant.components.fan import ( + ATTR_SPEED, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_SPEED, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import ( + ATTR_CURRENT_POWER_W, + DOMAIN as SWITCH_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + CONF_ENTITIES, + CONF_NAME, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.setup import async_setup_component + +from tests.async_mock import AsyncMock, Mock, patch + +ENTITY_SWITCH = "switch.ac" +ENTITY_SWITCH_NAME = "A/C" +ENTITY_SWITCH_POWER = 400.0 +ENTITY_LIGHT = "light.bed_light" +ENTITY_LIGHT_NAME = "Bed Room Lights" +ENTITY_FAN = "fan.ceiling_fan" +ENTITY_FAN_NAME = "Ceiling Fan" +ENTITY_FAN_SPEED_LOW = 5 +ENTITY_FAN_SPEED_MED = 10 +ENTITY_FAN_SPEED_HIGH = 50 +ENTITY_SENSOR = "sensor.outside_temperature" +ENTITY_SENSOR_NAME = "Power Sensor" + +CONFIG = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_SWITCH: { + CONF_NAME: ENTITY_SWITCH_NAME, + CONF_POWER: ENTITY_SWITCH_POWER, + }, + ENTITY_LIGHT: { + CONF_NAME: ENTITY_LIGHT_NAME, + CONF_POWER_ENTITY: ENTITY_SENSOR, + }, + ENTITY_FAN: { + CONF_POWER: "{% if is_state_attr('" + + ENTITY_FAN + + "','speed', 'low') %} " + + str(ENTITY_FAN_SPEED_LOW) + + "{% elif is_state_attr('" + + ENTITY_FAN + + "','speed', 'medium') %} " + + str(ENTITY_FAN_SPEED_MED) + + "{% elif is_state_attr('" + + ENTITY_FAN + + "','speed', 'high') %} " + + str(ENTITY_FAN_SPEED_HIGH) + + "{% endif %}" + }, + } + } +} + +CONFIG_SWITCH = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_SWITCH: { + CONF_NAME: ENTITY_SWITCH_NAME, + CONF_POWER: ENTITY_SWITCH_POWER, + }, + } + } +} + +CONFIG_SWITCH_NO_POWER = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_SWITCH: {}, + } + } +} + +CONFIG_LIGHT = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_LIGHT: { + CONF_NAME: ENTITY_LIGHT_NAME, + CONF_POWER_ENTITY: ENTITY_SENSOR, + }, + } + } +} + +CONFIG_FAN = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_FAN: { + CONF_POWER: "{% if is_state_attr('" + + ENTITY_FAN + + "','speed', 'low') %} " + + str(ENTITY_FAN_SPEED_LOW) + + "{% elif is_state_attr('" + + ENTITY_FAN + + "','speed', 'medium') %} " + + str(ENTITY_FAN_SPEED_MED) + + "{% elif is_state_attr('" + + ENTITY_FAN + + "','speed', 'high') %} " + + str(ENTITY_FAN_SPEED_HIGH) + + "{% endif %}" + }, + } + } +} + +CONFIG_SENSOR = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_SENSOR: {CONF_NAME: ENTITY_SENSOR_NAME}, + } + } +} + + +def nested_value(ndict, *keys): + """Return a nested dict value or None if it doesn't exist.""" + if len(keys) == 0: + return ndict + key = keys[0] + if not isinstance(ndict, dict) or key not in ndict: + return None + return nested_value(ndict[key], *keys[1:]) + + +async def test_setup(hass): + """Test that devices are reported correctly.""" + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG) is True + + +async def test_float(hass): + """Test a configuration using a simple float.""" + config = CONFIG_SWITCH[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + {SWITCH_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_SWITCH) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + # Turn switch on + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + + switch = hass.states.get(ENTITY_SWITCH) + assert switch.state == STATE_ON + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SWITCH_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_SWITCH_POWER) + + # Turn off + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SWITCH_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_switch_power(hass): + """Test a configuration using a simple float.""" + config = CONFIG_SWITCH_NO_POWER[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + {SWITCH_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_SWITCH_NO_POWER) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + # Turn switch on + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + + switch = hass.states.get(ENTITY_SWITCH) + assert switch.state == STATE_ON + power = switch.attributes[ATTR_CURRENT_POWER_W] + assert power == 100 + assert switch.name == "AC" + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + + assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, power) + + hass.states.async_set( + ENTITY_SWITCH, + STATE_ON, + attributes={ATTR_CURRENT_POWER_W: 120, ATTR_FRIENDLY_NAME: "AC"}, + ) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + + assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 120) + + # Turn off + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_template(hass): + """Test a configuration using a complex template.""" + config = CONFIG_FAN[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, FAN_DOMAIN, {FAN_DOMAIN: {"platform": "demo"}} + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_FAN) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + # Turn all devices on to known state + await hass.services.async_call( + FAN_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_FAN}, blocking=True + ) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "low"}, + blocking=True, + ) + + fan = hass.states.get(ENTITY_FAN) + assert fan.state == STATE_ON + + # Fan low: + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_FAN_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_FAN_SPEED_LOW) + + # Fan High: + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "high"}, + blocking=True, + ) + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_FAN_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_FAN_SPEED_HIGH) + + # Fan off: + await hass.services.async_call( + FAN_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_FAN}, blocking=True + ) + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_FAN_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_sensor(hass): + """Test a configuration using a sensor in a template.""" + config = CONFIG_LIGHT[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}} + ) + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_LIGHT) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT}, blocking=True + ) + hass.states.async_set(ENTITY_SENSOR, 35) + + light = hass.states.get(ENTITY_LIGHT) + assert light.state == STATE_ON + sensor = hass.states.get(ENTITY_SENSOR) + assert sensor.state == "35" + + # light + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_LIGHT_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 35) + + # change power sensor + hass.states.async_set(ENTITY_SENSOR, 40) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_LIGHT_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 40) + + # report 0 if device is off + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_LIGHT}, blocking=True + ) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_LIGHT_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_sensor_state(hass): + """Test a configuration using a sensor in a template.""" + config = CONFIG_SENSOR[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_SENSOR) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + hass.states.async_set(ENTITY_SENSOR, 35) + + sensor = hass.states.get(ENTITY_SENSOR) + assert sensor.state == "35" + + # sensor + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SENSOR_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 35) + + # change power sensor + hass.states.async_set(ENTITY_SENSOR, 40) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SENSOR_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 40) + + # report 0 if device is off + hass.states.async_set(ENTITY_SENSOR, 0) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SENSOR_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_multiple_devices(hass): + """Test that devices are reported correctly.""" + config = CONFIG[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": "demo"}} + ) + assert await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}} + ) + assert await async_setup_component( + hass, FAN_DOMAIN, {FAN_DOMAIN: {"platform": "demo"}} + ) + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await emulated_kasa.async_setup(hass, CONFIG) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + # Turn all devices on to known state + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT}, blocking=True + ) + hass.states.async_set(ENTITY_SENSOR, 35) + await hass.services.async_call( + FAN_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_FAN}, blocking=True + ) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "medium"}, + blocking=True, + ) + + # All of them should now be on + switch = hass.states.get(ENTITY_SWITCH) + assert switch.state == STATE_ON + light = hass.states.get(ENTITY_LIGHT) + assert light.state == STATE_ON + sensor = hass.states.get(ENTITY_SENSOR) + assert sensor.state == "35" + fan = hass.states.get(ENTITY_FAN) + assert fan.state == STATE_ON + + plug_it = emulated_kasa.get_plug_devices(hass, config) + # switch + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SWITCH_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_SWITCH_POWER) + + # light + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_LIGHT_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 35) + + # fan + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_FAN_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_FAN_SPEED_MED) + + # No more devices + assert next(plug_it, None) is None From 52c09396e004e3fb2f414c94fae08bfd097bde36 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 5 Sep 2020 15:25:22 +0200 Subject: [PATCH 663/862] Fix monoprice option flow test (#39685) --- tests/components/monoprice/test_config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 49a7ed27e5a..e2aae6eddaa 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -97,15 +97,16 @@ async def test_options_flow(hass): config_entry = MockConfigEntry( domain=DOMAIN, - # unique_id="abcde12345", data=conf, - # options={CONF_SHOW_ON_MAP: True}, ) config_entry.add_to_hass(hass) with patch( "homeassistant.components.monoprice.async_setup_entry", return_value=True ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM From b860caa631acc9eba80d495a812b76d09867cbe1 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Sat, 5 Sep 2020 07:26:01 -0700 Subject: [PATCH 664/862] Add iSmartGate support (#39437) * Add iSmartGate support. * Addressing PR feedback. * More PR feedback cleanups. --- .../components/gogogate2/__init__.py | 14 + homeassistant/components/gogogate2/common.py | 34 +- .../components/gogogate2/config_flow.py | 49 +- homeassistant/components/gogogate2/const.py | 2 + homeassistant/components/gogogate2/cover.py | 86 ++- .../components/gogogate2/manifest.json | 4 +- .../components/gogogate2/strings.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gogogate2/common.py | 163 ------ tests/components/gogogate2/conftest.py | 18 - .../components/gogogate2/test_config_flow.py | 99 ++-- tests/components/gogogate2/test_cover.py | 506 ++++++++---------- tests/components/gogogate2/test_init.py | 75 ++- 14 files changed, 458 insertions(+), 598 deletions(-) delete mode 100644 tests/components/gogogate2/common.py delete mode 100644 tests/components/gogogate2/conftest.py diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index 36f623f7895..93f000e6a3a 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -1,10 +1,12 @@ """The gogogate2 component.""" from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .common import get_data_update_coordinator +from .const import DEVICE_TYPE_GOGOGATE2 async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: @@ -14,6 +16,18 @@ async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Do setup of Gogogate2.""" + + # Update the config entry. + config_updates = {} + if CONF_DEVICE not in config_entry.data: + config_updates["data"] = { + **config_entry.data, + **{CONF_DEVICE: DEVICE_TYPE_GOGOGATE2}, + } + + if config_updates: + hass.config_entries.async_update_entry(config_entry, **config_updates) + data_update_coordinator = get_data_update_coordinator(hass, config_entry) await data_update_coordinator.async_refresh() diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 10adc9c61b3..bf333ab5898 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -4,16 +4,21 @@ import logging from typing import Awaitable, Callable, NamedTuple, Optional import async_timeout -from gogogate2_api import GogoGate2Api -from gogogate2_api.common import Door +from gogogate2_api import AbstractGateApi, GogoGate2Api, ISmartGateApi +from gogogate2_api.common import AbstractDoor from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICE, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_UPDATE_COORDINATOR, DOMAIN +from .const import DATA_UPDATE_COORDINATOR, DEVICE_TYPE_ISMARTGATE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -23,17 +28,17 @@ class StateData(NamedTuple): config_unique_id: str unique_id: Optional[str] - door: Optional[Door] + door: Optional[AbstractDoor] -class GogoGateDataUpdateCoordinator(DataUpdateCoordinator): +class DeviceDataUpdateCoordinator(DataUpdateCoordinator): """Manages polling for state changes from the device.""" def __init__( self, hass: HomeAssistant, logger: logging.Logger, - api: GogoGate2Api, + api: AbstractGateApi, *, name: str, update_interval: timedelta, @@ -55,7 +60,7 @@ class GogoGateDataUpdateCoordinator(DataUpdateCoordinator): def get_data_update_coordinator( hass: HomeAssistant, config_entry: ConfigEntry -) -> GogoGateDataUpdateCoordinator: +) -> DeviceDataUpdateCoordinator: """Get an update coordinator.""" hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(config_entry.entry_id, {}) @@ -73,7 +78,7 @@ def get_data_update_coordinator( f"Error communicating with API: {exception}" ) from exception - config_entry_data[DATA_UPDATE_COORDINATOR] = GogoGateDataUpdateCoordinator( + config_entry_data[DATA_UPDATE_COORDINATOR] = DeviceDataUpdateCoordinator( hass, _LOGGER, api, @@ -87,14 +92,19 @@ def get_data_update_coordinator( return config_entry_data[DATA_UPDATE_COORDINATOR] -def cover_unique_id(config_entry: ConfigEntry, door: Door) -> str: +def cover_unique_id(config_entry: ConfigEntry, door: AbstractDoor) -> str: """Generate a cover entity unique id.""" return f"{config_entry.unique_id}_{door.door_id}" -def get_api(config_data: dict) -> GogoGate2Api: +def get_api(config_data: dict) -> AbstractGateApi: """Get an api object for config data.""" - return GogoGate2Api( + gate_class = GogoGate2Api + + if config_data[CONF_DEVICE] == DEVICE_TYPE_ISMARTGATE: + gate_class = ISmartGateApi + + return gate_class( config_data[CONF_IP_ADDRESS], config_data[CONF_USERNAME], config_data[CONF_PASSWORD], diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index bca340fa62b..4a70a822023 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -1,16 +1,23 @@ """Config flow for Gogogate2.""" +import dataclasses import logging import re -from gogogate2_api.common import ApiError -from gogogate2_api.const import ApiErrorCode +from gogogate2_api.common import AbstractInfoResponse, ApiError +from gogogate2_api.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow -from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICE, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_USERNAME, +) from .common import get_api +from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -36,15 +43,35 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): if user_input: api = get_api(user_input) try: - data = await self.hass.async_add_executor_job(api.info) + data: AbstractInfoResponse = await self.hass.async_add_executor_job( + api.info + ) + data_dict = dataclasses.asdict(data) + title = data_dict.get( + "gogogatename", data_dict.get("ismartgatename", "Cover") + ) await self.async_set_unique_id(re.sub("\\..*$", "", data.remoteaccess)) - return self.async_create_entry(title=data.gogogatename, data=user_input) + return self.async_create_entry(title=title, data=user_input) except ApiError as api_error: - if api_error.code in ( - ApiErrorCode.CREDENTIALS_NOT_SET, - ApiErrorCode.CREDENTIALS_INCORRECT, - ): + device_type = user_input[CONF_DEVICE] + is_invalid_auth = ( + device_type == DEVICE_TYPE_GOGOGATE2 + and api_error.code + in ( + GogoGate2ApiErrorCode.CREDENTIALS_NOT_SET, + GogoGate2ApiErrorCode.CREDENTIALS_INCORRECT, + ) + ) or ( + device_type == DEVICE_TYPE_ISMARTGATE + and api_error.code + in ( + ISmartGateApiErrorCode.CREDENTIALS_NOT_SET, + ISmartGateApiErrorCode.CREDENTIALS_INCORRECT, + ) + ) + + if is_invalid_auth: errors["base"] = "invalid_auth" else: errors["base"] = "cannot_connect" @@ -59,6 +86,10 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { + vol.Required( + CONF_DEVICE, + default=user_input.get(CONF_DEVICE, DEVICE_TYPE_GOGOGATE2), + ): vol.In((DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE)), vol.Required( CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS, "") ): str, diff --git a/homeassistant/components/gogogate2/const.py b/homeassistant/components/gogogate2/const.py index 359de5f750c..5c0ef55ff3f 100644 --- a/homeassistant/components/gogogate2/const.py +++ b/homeassistant/components/gogogate2/const.py @@ -2,3 +2,5 @@ DOMAIN = "gogogate2" DATA_UPDATE_COORDINATOR = "data_update_coordinator" +DEVICE_TYPE_GOGOGATE2 = "gogogate2" +DEVICE_TYPE_ISMARTGATE = "ismartgate" diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index a26bdbc3c8e..bd7f176856b 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -2,7 +2,12 @@ import logging from typing import Callable, List, Optional -from gogogate2_api.common import Door, DoorStatus, get_configured_doors, get_door_by_id +from gogogate2_api.common import ( + AbstractDoor, + DoorStatus, + get_configured_doors, + get_door_by_id, +) import voluptuous as vol from homeassistant.components.cover import ( @@ -12,17 +17,23 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import ( + CONF_DEVICE, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .common import ( - GogoGateDataUpdateCoordinator, + DeviceDataUpdateCoordinator, cover_unique_id, get_data_update_coordinator, ) -from .const import DOMAIN +from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -30,6 +41,9 @@ _LOGGER = logging.getLogger(__name__) COVER_SCHEMA = vol.Schema( { vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_DEVICE, default=DEVICE_TYPE_GOGOGATE2): vol.In( + (DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE) + ), vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, } @@ -57,39 +71,29 @@ async def async_setup_entry( async_add_entities( [ - Gogogate2Cover(config_entry, data_update_coordinator, door) + DeviceCover(config_entry, data_update_coordinator, door) for door in get_configured_doors(data_update_coordinator.data) ] ) -class Gogogate2Cover(CoverEntity): +class DeviceCover(CoordinatorEntity, CoverEntity): """Cover entity for goggate2.""" def __init__( self, config_entry: ConfigEntry, - data_update_coordinator: GogoGateDataUpdateCoordinator, - door: Door, + data_update_coordinator: DeviceDataUpdateCoordinator, + door: AbstractDoor, ) -> None: """Initialize the object.""" + super().__init__(data_update_coordinator) self._config_entry = config_entry - self._data_update_coordinator = data_update_coordinator self._door = door self._api = data_update_coordinator.api self._unique_id = cover_unique_id(config_entry, door) self._is_available = True - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._is_available - - @property - def should_poll(self) -> bool: - """Return False as the data manager handles dispatching data.""" - return False - @property def unique_id(self) -> Optional[str]: """Return a unique ID.""" @@ -98,14 +102,16 @@ class Gogogate2Cover(CoverEntity): @property def name(self): """Return the name of the door.""" - return self._door.name + return self._get_door().name @property def is_closed(self): """Return true if cover is closed, else False.""" - if self._door.status == DoorStatus.OPENED: + door = self._get_door() + + if door.status == DoorStatus.OPENED: return False - if self._door.status == DoorStatus.CLOSED: + if door.status == DoorStatus.CLOSED: return True return None @@ -122,36 +128,24 @@ class Gogogate2Cover(CoverEntity): async def async_open_cover(self, **kwargs): """Open the door.""" - await self.hass.async_add_executor_job(self._api.open_door, self._door.door_id) + await self.hass.async_add_executor_job( + self._api.open_door, self._get_door().door_id + ) async def async_close_cover(self, **kwargs): """Close the door.""" - await self.hass.async_add_executor_job(self._api.close_door, self._door.door_id) + await self.hass.async_add_executor_job( + self._api.close_door, self._get_door().door_id + ) @property def state_attributes(self): """Return the state attributes.""" attrs = super().state_attributes - attrs["door_id"] = self._door.door_id + attrs["door_id"] = self._get_door().door_id return attrs - @callback - def async_on_data_updated(self) -> None: - """Receive data from data dispatcher.""" - if not self._data_update_coordinator.last_update_success: - self._is_available = False - self.async_write_ha_state() - return - - door = get_door_by_id(self._door.door_id, self._data_update_coordinator.data) - - # Set the state. - self._door = door - self._is_available = True - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Register update dispatcher.""" - self.async_on_remove( - self._data_update_coordinator.async_add_listener(self.async_on_data_updated) - ) + def _get_door(self) -> AbstractDoor: + door = get_door_by_id(self._door.door_id, self.coordinator.data) + self._door = door or self._door + return self._door diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 588d68484f2..08c8c64b139 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -1,8 +1,8 @@ { "domain": "gogogate2", - "name": "Gogogate2", + "name": "Gogogate2 and iSmartGate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["gogogate2-api==1.0.4"], + "requirements": ["gogogate2-api==2.0.0"], "codeowners": ["@vangorra"] } diff --git a/homeassistant/components/gogogate2/strings.json b/homeassistant/components/gogogate2/strings.json index bbd4e8d80d1..47a53d0d320 100644 --- a/homeassistant/components/gogogate2/strings.json +++ b/homeassistant/components/gogogate2/strings.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "title": "Setup GogoGate2", + "title": "Setup GogoGate2 or iSmartGate", "description": "Provide requisite information below.", "data": { "ip_address": "IP Address", diff --git a/requirements_all.txt b/requirements_all.txt index 93590f7b386..46f40e086f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -668,7 +668,7 @@ glances_api==0.2.0 gntp==1.0.3 # homeassistant.components.gogogate2 -gogogate2-api==1.0.4 +gogogate2-api==2.0.0 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75249673c2c..2f571c3b220 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -334,7 +334,7 @@ gios==0.1.4 glances_api==0.2.0 # homeassistant.components.gogogate2 -gogogate2-api==1.0.4 +gogogate2-api==2.0.0 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/tests/components/gogogate2/common.py b/tests/components/gogogate2/common.py deleted file mode 100644 index 84999cb0e29..00000000000 --- a/tests/components/gogogate2/common.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Common test code.""" -from typing import List, NamedTuple, Optional -from unittest.mock import MagicMock, Mock - -from gogogate2_api import GogoGate2Api, InfoResponse -from gogogate2_api.common import Door, DoorMode, DoorStatus, Network, Outputs, Wifi - -from homeassistant.components import persistent_notification -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN -from homeassistant.components.gogogate2 import async_unload_entry -from homeassistant.components.gogogate2.common import ( - GogoGateDataUpdateCoordinator, - get_data_update_coordinator, -) -import homeassistant.components.gogogate2.const as const -from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN -from homeassistant.config import async_process_ha_core_config -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM -from homeassistant.setup import async_setup_component - -INFO_RESPONSE = InfoResponse( - user="user1", - gogogatename="gogogatename1", - model="", - apiversion="", - remoteaccessenabled=False, - remoteaccess="abcdefg.my-gogogate.com", - firmwareversion="", - apicode="API_CODE", - door1=Door( - door_id=1, - permission=True, - name="Door1", - mode=DoorMode.GARAGE, - status=DoorStatus.OPENED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - ), - door2=Door( - door_id=2, - permission=True, - name=None, - mode=DoorMode.GARAGE, - status=DoorStatus.OPENED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - ), - door3=Door( - door_id=3, - permission=True, - name="Door3", - mode=DoorMode.GARAGE, - status=DoorStatus.OPENED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - ), - outputs=Outputs(output1=True, output2=False, output3=True), - network=Network(ip=""), - wifi=Wifi(SSID="", linkquality="", signal=""), -) - - -class ComponentData(NamedTuple): - """Test data for a mocked component.""" - - api: GogoGate2Api - data_update_coordinator: GogoGateDataUpdateCoordinator - - -class ComponentFactory: - """Manages the setup and unloading of the withing component and profiles.""" - - def __init__(self, hass: HomeAssistant, gogogate_api_mock: Mock) -> None: - """Initialize the object.""" - self._hass = hass - self._gogogate_api_mock = gogogate_api_mock - - @property - def api_class_mock(self): - """Get the api class mock.""" - return self._gogogate_api_mock - - async def configure_component( - self, cover_config: Optional[List[dict]] = None - ) -> None: - """Configure the component.""" - hass_config = { - "homeassistant": {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, - "cover": cover_config or [], - } - - await async_process_ha_core_config(self._hass, hass_config.get("homeassistant")) - assert await async_setup_component(self._hass, HA_DOMAIN, {}) - assert await async_setup_component( - self._hass, persistent_notification.DOMAIN, {} - ) - assert await async_setup_component(self._hass, COVER_DOMAIN, hass_config) - assert await async_setup_component(self._hass, const.DOMAIN, hass_config) - await self._hass.async_block_till_done() - - async def run_config_flow( - self, config_data: dict, api_mock: Optional[GogoGate2Api] = None - ) -> ComponentData: - """Run a config flow.""" - if api_mock is None: - api_mock: GogoGate2Api = MagicMock(spec=GogoGate2Api) - api_mock.info.return_value = INFO_RESPONSE - - self._gogogate_api_mock.reset_mocks() - self._gogogate_api_mock.return_value = api_mock - - result = await self._hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": SOURCE_USER} - ) - assert result - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await self._hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=config_data, - ) - assert result - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == config_data - - await self._hass.async_block_till_done() - - config_entry = next( - iter( - entry - for entry in self._hass.config_entries.async_entries(const.DOMAIN) - if entry.unique_id == "abcdefg" - ) - ) - - return ComponentData( - api=api_mock, - data_update_coordinator=get_data_update_coordinator( - self._hass, config_entry - ), - ) - - async def unload(self) -> None: - """Unload all config entries.""" - config_entries = self._hass.config_entries.async_entries(const.DOMAIN) - for config_entry in config_entries: - await async_unload_entry(self._hass, config_entry) - - await self._hass.async_block_till_done() - assert not self._hass.states.async_entity_ids("gogogate") diff --git a/tests/components/gogogate2/conftest.py b/tests/components/gogogate2/conftest.py deleted file mode 100644 index 31e85c5f14e..00000000000 --- a/tests/components/gogogate2/conftest.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Fixtures for tests.""" - -import pytest - -from homeassistant.core import HomeAssistant - -from .common import ComponentFactory - -from tests.async_mock import patch - - -@pytest.fixture() -def component_factory(hass: HomeAssistant): - """Return a factory for initializing the gogogate2 api.""" - with patch( - "homeassistant.components.gogogate2.common.GogoGate2Api" - ) as gogogate2_api_mock: - yield ComponentFactory(hass, gogogate2_api_mock) diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 55de8701b61..8c07a3fc023 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -1,65 +1,66 @@ """Tests for the GogoGate2 component.""" from gogogate2_api import GogoGate2Api from gogogate2_api.common import ApiError -from gogogate2_api.const import ApiErrorCode +from gogogate2_api.const import GogoGate2ApiErrorCode +from homeassistant.components.gogogate2.const import DEVICE_TYPE_GOGOGATE2 from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_DEVICE, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_FORM -from .common import ComponentFactory - from tests.async_mock import MagicMock, patch +@patch("homeassistant.components.gogogate2.async_setup", return_value=True) +@patch("homeassistant.components.gogogate2.async_setup_entry", return_value=True) +@patch("homeassistant.components.gogogate2.common.GogoGate2Api") async def test_auth_fail( - hass: HomeAssistant, component_factory: ComponentFactory + gogogate2api_mock, async_setup_entry_mock, async_setup_mock, hass: HomeAssistant ) -> None: """Test authorization failures.""" - api_mock: GogoGate2Api = MagicMock(spec=GogoGate2Api) + api: GogoGate2Api = MagicMock(spec=GogoGate2Api) + gogogate2api_mock.return_value = api - with patch( - "homeassistant.components.gogogate2.async_setup", return_value=True - ), patch( - "homeassistant.components.gogogate2.async_setup_entry", - return_value=True, - ): - await component_factory.configure_component() - component_factory.api_class_mock.return_value = api_mock + api.reset_mock() + api.info.side_effect = ApiError(GogoGate2ApiErrorCode.CREDENTIALS_INCORRECT, "blah") + result = await hass.config_entries.flow.async_init( + "gogogate2", context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, + CONF_IP_ADDRESS: "127.0.0.2", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == { + "base": "invalid_auth", + } - api_mock.reset_mock() - api_mock.info.side_effect = ApiError(ApiErrorCode.CREDENTIALS_INCORRECT, "blah") - result = await hass.config_entries.flow.async_init( - "gogogate2", context={"source": SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_IP_ADDRESS: "127.0.0.2", - CONF_USERNAME: "user0", - CONF_PASSWORD: "password0", - }, - ) - assert result - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == { - "base": "invalid_auth", - } - - api_mock.reset_mock() - api_mock.info.side_effect = Exception("Generic connection error.") - result = await hass.config_entries.flow.async_init( - "gogogate2", context={"source": SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_IP_ADDRESS: "127.0.0.2", - CONF_USERNAME: "user0", - CONF_PASSWORD: "password0", - }, - ) - assert result - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} + api.reset_mock() + api.info.side_effect = Exception("Generic connection error.") + result = await hass.config_entries.flow.async_init( + "gogogate2", context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, + CONF_IP_ADDRESS: "127.0.0.2", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index b754b88c05e..dd19d408741 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -1,65 +1,90 @@ """Tests for the GogoGate2 component.""" -from gogogate2_api import GogoGate2Api +from datetime import timedelta + +from gogogate2_api import GogoGate2Api, ISmartGateApi from gogogate2_api.common import ( - ActivateResponse, ApiError, - Door, DoorMode, DoorStatus, - InfoResponse, + GogoGate2ActivateResponse, + GogoGate2Door, + GogoGate2InfoResponse, + ISmartGateDoor, + ISmartGateInfoResponse, Network, Outputs, Wifi, ) from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.gogogate2.const import ( + DEVICE_TYPE_GOGOGATE2, + DEVICE_TYPE_ISMARTGATE, + DOMAIN, +) from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN +from homeassistant.config import async_process_ha_core_config +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( + CONF_DEVICE, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_PLATFORM, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_METRIC, CONF_USERNAME, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -from .common import ComponentFactory - -from tests.async_mock import MagicMock +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry, async_fire_time_changed -async def test_import_fail( - hass: HomeAssistant, component_factory: ComponentFactory -) -> None: +@patch("homeassistant.components.gogogate2.common.GogoGate2Api") +async def test_import_fail(gogogate2api_mock, hass: HomeAssistant) -> None: """Test the failure to import.""" api = MagicMock(spec=GogoGate2Api) api.info.side_effect = ApiError(22, "Error") + gogogate2api_mock.return_value = api - component_factory.api_class_mock.return_value = api - - await component_factory.configure_component( - cover_config=[ + hass_config = { + HA_DOMAIN: {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, + COVER_DOMAIN: [ { CONF_PLATFORM: "gogogate2", CONF_NAME: "cover0", + CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, CONF_IP_ADDRESS: "127.0.1.0", CONF_USERNAME: "user0", CONF_PASSWORD: "password0", } - ] - ) + ], + } + + await async_process_ha_core_config(hass, hass_config[HA_DOMAIN]) + assert await async_setup_component(hass, HA_DOMAIN, {}) + assert await async_setup_component(hass, COVER_DOMAIN, hass_config) + await hass.async_block_till_done() entity_ids = hass.states.async_entity_ids(COVER_DOMAIN) assert not entity_ids -async def test_import(hass: HomeAssistant, component_factory: ComponentFactory) -> None: +@patch("homeassistant.components.gogogate2.common.GogoGate2Api") +@patch("homeassistant.components.gogogate2.common.ISmartGateApi") +async def test_import( + ismartgateapi_mock, gogogate2api_mock, hass: HomeAssistant +) -> None: """Test importing of file based config.""" api0 = MagicMock(spec=GogoGate2Api) - api0.info.return_value = InfoResponse( + api0.info.return_value = GogoGate2InfoResponse( user="user1", gogogatename="gogogatename0", model="", @@ -68,7 +93,7 @@ async def test_import(hass: HomeAssistant, component_factory: ComponentFactory) remoteaccess="abc123.blah.blah", firmwareversion="", apicode="", - door1=Door( + door1=GogoGate2Door( door_id=1, permission=True, name="Door1", @@ -80,7 +105,7 @@ async def test_import(hass: HomeAssistant, component_factory: ComponentFactory) events=2, temperature=None, ), - door2=Door( + door2=GogoGate2Door( door_id=2, permission=True, name=None, @@ -92,7 +117,7 @@ async def test_import(hass: HomeAssistant, component_factory: ComponentFactory) events=0, temperature=None, ), - door3=Door( + door3=GogoGate2Door( door_id=3, permission=True, name=None, @@ -108,18 +133,21 @@ async def test_import(hass: HomeAssistant, component_factory: ComponentFactory) network=Network(ip=""), wifi=Wifi(SSID="", linkquality="", signal=""), ) + gogogate2api_mock.return_value = api0 - api1 = MagicMock(spec=GogoGate2Api) - api1.info.return_value = InfoResponse( + api1 = MagicMock(spec=ISmartGateApi) + api1.info.return_value = ISmartGateInfoResponse( user="user1", - gogogatename="gogogatename0", + ismartgatename="ismartgatename0", model="", apiversion="", remoteaccessenabled=False, - remoteaccess="321bca.blah.blah", + remoteaccess="abc321.blah.blah", firmwareversion="", - apicode="", - door1=Door( + pin=123, + lang="en", + newfirmware=False, + door1=ISmartGateDoor( door_id=1, permission=True, name="Door1", @@ -130,50 +158,52 @@ async def test_import(hass: HomeAssistant, component_factory: ComponentFactory) camera=False, events=2, temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, ), - door2=Door( - door_id=2, + door2=ISmartGateDoor( + door_id=1, permission=True, name=None, mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, + status=DoorStatus.CLOSED, sensor=True, sensorid=None, camera=False, - events=0, + events=2, temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, ), - door3=Door( - door_id=3, + door3=ISmartGateDoor( + door_id=1, permission=True, name=None, mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, + status=DoorStatus.CLOSED, sensor=True, sensorid=None, camera=False, - events=0, + events=2, temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, ), - outputs=Outputs(output1=True, output2=False, output3=True), network=Network(ip=""), wifi=Wifi(SSID="", linkquality="", signal=""), ) + ismartgateapi_mock.return_value = api1 - def new_api(ip_address: str, username: str, password: str) -> GogoGate2Api: - if ip_address == "127.0.1.0": - return api0 - if ip_address == "127.0.1.1": - return api1 - raise Exception(f"Untested ip address {ip_address}") - - component_factory.api_class_mock.side_effect = new_api - - await component_factory.configure_component( - cover_config=[ + hass_config = { + HA_DOMAIN: {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, + COVER_DOMAIN: [ { CONF_PLATFORM: "gogogate2", CONF_NAME: "cover0", + CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, CONF_IP_ADDRESS: "127.0.1.0", CONF_USERNAME: "user0", CONF_PASSWORD: "password0", @@ -181,274 +211,148 @@ async def test_import(hass: HomeAssistant, component_factory: ComponentFactory) { CONF_PLATFORM: "gogogate2", CONF_NAME: "cover1", + CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, CONF_IP_ADDRESS: "127.0.1.1", CONF_USERNAME: "user1", CONF_PASSWORD: "password1", }, - ] - ) + ], + } + + await async_process_ha_core_config(hass, hass_config[HA_DOMAIN]) + assert await async_setup_component(hass, HA_DOMAIN, {}) + assert await async_setup_component(hass, COVER_DOMAIN, hass_config) + await hass.async_block_till_done() + entity_ids = hass.states.async_entity_ids(COVER_DOMAIN) assert entity_ids is not None assert len(entity_ids) == 2 assert "cover.door1" in entity_ids assert "cover.door1_2" in entity_ids - await component_factory.unload() +@patch("homeassistant.components.gogogate2.common.GogoGate2Api") +async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: + """Test open and close and data update.""" -async def test_cover_update( - hass: HomeAssistant, component_factory: ComponentFactory -) -> None: - """Test cover.""" - await component_factory.configure_component() - component_data = await component_factory.run_config_flow( - config_data={ - CONF_IP_ADDRESS: "127.0.0.2", - CONF_USERNAME: "user0", - CONF_PASSWORD: "password0", - } + def info_response(door_status: DoorStatus) -> GogoGate2InfoResponse: + return GogoGate2InfoResponse( + user="user1", + gogogatename="gogogatename0", + model="", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc123.blah.blah", + firmwareversion="", + apicode="", + door1=GogoGate2Door( + door_id=1, + permission=True, + name="Door1", + mode=DoorMode.GARAGE, + status=door_status, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + door2=GogoGate2Door( + door_id=2, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + door3=GogoGate2Door( + door_id=3, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) + + api = MagicMock(GogoGate2Api) + api.activate.return_value = GogoGate2ActivateResponse(result=True) + api.info.return_value = info_response(DoorStatus.OPENED) + gogogat2api_mock.return_value = api + + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_IP_ADDRESS: "127.0.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + }, ) + config_entry.add_to_hass(hass) - assert hass.states.async_entity_ids(COVER_DOMAIN) - - state = hass.states.get("cover.door1") - assert state - assert state.state == STATE_OPEN - assert state.attributes["friendly_name"] == "Door1" - assert state.attributes["supported_features"] == 3 - assert state.attributes["device_class"] == "garage" - - component_data.data_update_coordinator.api.info.return_value = InfoResponse( - user="user1", - gogogatename="gogogatename0", - model="", - apiversion="", - remoteaccessenabled=False, - remoteaccess="abc123.blah.blah", - firmwareversion="", - apicode="", - door1=Door( - door_id=1, - permission=True, - name="Door1", - mode=DoorMode.GARAGE, - status=DoorStatus.OPENED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - ), - door2=Door( - door_id=2, - permission=True, - name=None, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - ), - door3=Door( - door_id=3, - permission=True, - name=None, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - ), - outputs=Outputs(output1=True, output2=False, output3=True), - network=Network(ip=""), - wifi=Wifi(SSID="", linkquality="", signal=""), - ) - await component_data.data_update_coordinator.async_refresh() - await hass.async_block_till_done() - state = hass.states.get("cover.door1") - assert state - assert state.state == STATE_OPEN - - component_data.data_update_coordinator.api.info.return_value = InfoResponse( - user="user1", - gogogatename="gogogatename0", - model="", - apiversion="", - remoteaccessenabled=False, - remoteaccess="abc123.blah.blah", - firmwareversion="", - apicode="", - door1=Door( - door_id=1, - permission=True, - name="Door1", - mode=DoorMode.GARAGE, - status=DoorStatus.CLOSED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - ), - door2=Door( - door_id=2, - permission=True, - name=None, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - ), - door3=Door( - door_id=3, - permission=True, - name=None, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - ), - outputs=Outputs(output1=True, output2=False, output3=True), - network=Network(ip=""), - wifi=Wifi(SSID="", linkquality="", signal=""), - ) - await component_data.data_update_coordinator.async_refresh() - await hass.async_block_till_done() - state = hass.states.get("cover.door1") - assert state - assert state.state == STATE_CLOSED - - -async def test_open_close( - hass: HomeAssistant, component_factory: ComponentFactory -) -> None: - """Test open and close.""" - closed_door_response = InfoResponse( - user="user1", - gogogatename="gogogatename0", - model="", - apiversion="", - remoteaccessenabled=False, - remoteaccess="abc123.blah.blah", - firmwareversion="", - apicode="", - door1=Door( - door_id=1, - permission=True, - name="Door1", - mode=DoorMode.GARAGE, - status=DoorStatus.CLOSED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - ), - door2=Door( - door_id=2, - permission=True, - name=None, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - ), - door3=Door( - door_id=3, - permission=True, - name=None, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - ), - outputs=Outputs(output1=True, output2=False, output3=True), - network=Network(ip=""), - wifi=Wifi(SSID="", linkquality="", signal=""), - ) - - await component_factory.configure_component() assert hass.states.get("cover.door1") is None - - component_data = await component_factory.run_config_flow( - config_data={ - CONF_IP_ADDRESS: "127.0.0.2", - CONF_USERNAME: "user0", - CONF_PASSWORD: "password0", - } - ) - - component_data.api.activate.return_value = ActivateResponse(result=True) - + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_OPEN + + api.info.return_value = info_response(DoorStatus.CLOSED) await hass.services.async_call( COVER_DOMAIN, "close_cover", service_data={"entity_id": "cover.door1"}, ) - await hass.async_block_till_done() - component_data.api.close_door.assert_called_with(1) - - component_data.data_update_coordinator.api.info.return_value = closed_door_response - await component_data.data_update_coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED + api.close_door.assert_called_with(1) - # Assert mid state changed when new status is received. + api.info.return_value = info_response(DoorStatus.OPENED) await hass.services.async_call( COVER_DOMAIN, "open_cover", service_data={"entity_id": "cover.door1"}, ) + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() - component_data.api.open_door.assert_called_with(1) + assert hass.states.get("cover.door1").state == STATE_OPEN + api.open_door.assert_called_with(1) - # Assert the mid state does not change when the same status is returned. - component_data.data_update_coordinator.api.info.return_value = closed_door_response - await component_data.data_update_coordinator.async_refresh() - component_data.data_update_coordinator.api.info.return_value = closed_door_response - await component_data.data_update_coordinator.async_refresh() - - await component_data.data_update_coordinator.async_refresh() - await hass.services.async_call( - HA_DOMAIN, - "update_entity", - service_data={"entity_id": "cover.door1"}, - ) + api.info.return_value = info_response(DoorStatus.UNDEFINED) + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() - assert hass.states.get("cover.door1").state == STATE_CLOSED + assert hass.states.get("cover.door1").state == STATE_UNKNOWN + + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert not hass.states.async_entity_ids(DOMAIN) -async def test_availability( - hass: HomeAssistant, component_factory: ComponentFactory -) -> None: - """Test open and close.""" - closed_door_response = InfoResponse( +@patch("homeassistant.components.gogogate2.common.ISmartGateApi") +async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: + """Test availability.""" + closed_door_response = ISmartGateInfoResponse( user="user1", - gogogatename="gogogatename0", + ismartgatename="ismartgatename0", model="", apiversion="", remoteaccessenabled=False, remoteaccess="abc123.blah.blah", firmwareversion="", - apicode="", - door1=Door( + pin=123, + lang="en", + newfirmware=False, + door1=ISmartGateDoor( door_id=1, permission=True, name="Door1", @@ -459,8 +363,11 @@ async def test_availability( camera=False, events=2, temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, ), - door2=Door( + door2=ISmartGateDoor( door_id=2, permission=True, name=None, @@ -471,8 +378,11 @@ async def test_availability( camera=False, events=0, temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, ), - door3=Door( + door3=ISmartGateDoor( door_id=3, permission=True, name=None, @@ -483,31 +393,43 @@ async def test_availability( camera=False, events=0, temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, ), - outputs=Outputs(output1=True, output2=False, output3=True), network=Network(ip=""), wifi=Wifi(SSID="", linkquality="", signal=""), ) - await component_factory.configure_component() - assert hass.states.get("cover.door1") is None + api = MagicMock(ISmartGateApi) + api.info.return_value = closed_door_response + ismartgateapi_mock.return_value = api - component_data = await component_factory.run_config_flow( - config_data={ - CONF_IP_ADDRESS: "127.0.0.2", - CONF_USERNAME: "user0", - CONF_PASSWORD: "password0", - } + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, + CONF_IP_ADDRESS: "127.0.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + }, ) - assert hass.states.get("cover.door1").state == STATE_OPEN + config_entry.add_to_hass(hass) - component_data.api.info.side_effect = Exception("Error") - await component_data.data_update_coordinator.async_refresh() + assert hass.states.get("cover.door1") is None + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.get("cover.door1") + + api.info.side_effect = Exception("Error") + + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_UNAVAILABLE - component_data.api.info.side_effect = None - component_data.api.info.return_value = closed_door_response - await component_data.data_update_coordinator.async_refresh() + api.info.side_effect = None + api.info.return_value = closed_door_response + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED diff --git a/tests/components/gogogate2/test_init.py b/tests/components/gogogate2/test_init.py index 8788590407f..af8678300d1 100644 --- a/tests/components/gogogate2/test_init.py +++ b/tests/components/gogogate2/test_init.py @@ -1,8 +1,17 @@ """Tests for the GogoGate2 component.""" +from gogogate2_api import GogoGate2Api import pytest -from homeassistant.components.gogogate2 import async_setup_entry -from homeassistant.components.gogogate2.common import GogoGateDataUpdateCoordinator +from homeassistant.components.gogogate2 import DEVICE_TYPE_GOGOGATE2, async_setup_entry +from homeassistant.components.gogogate2.common import DeviceDataUpdateCoordinator +from homeassistant.components.gogogate2.const import DEVICE_TYPE_ISMARTGATE, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + CONF_DEVICE, + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -10,11 +19,69 @@ from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry +@patch("homeassistant.components.gogogate2.common.GogoGate2Api") +async def test_config_update(gogogate2api_mock, hass: HomeAssistant) -> None: + """Test config setup where the config is updated.""" + + api = MagicMock(GogoGate2Api) + api.info.side_effect = Exception("Error") + gogogate2api_mock.return_value = api + + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_IP_ADDRESS: "127.0.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + }, + ) + config_entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry_id=config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.data == { + CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, + CONF_IP_ADDRESS: "127.0.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + } + + +@patch("homeassistant.components.gogogate2.common.ISmartGateApi") +async def test_config_no_update(ismartgateapi_mock, hass: HomeAssistant) -> None: + """Test config setup where the data is not updated.""" + api = MagicMock(GogoGate2Api) + api.info.side_effect = Exception("Error") + ismartgateapi_mock.return_value = api + + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, + CONF_IP_ADDRESS: "127.0.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + }, + ) + config_entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(entry_id=config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.data == { + CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, + CONF_IP_ADDRESS: "127.0.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + } + + async def test_auth_fail(hass: HomeAssistant) -> None: """Test authorization failures.""" - coordinator_mock: GogoGateDataUpdateCoordinator = MagicMock( - spec=GogoGateDataUpdateCoordinator + coordinator_mock: DeviceDataUpdateCoordinator = MagicMock( + spec=DeviceDataUpdateCoordinator ) coordinator_mock.last_update_success = False From cdc93d7110fac0d8d1a5aaae1d38a287024f5654 Mon Sep 17 00:00:00 2001 From: Egor Shilyaev Date: Sat, 5 Sep 2020 19:40:39 +0500 Subject: [PATCH 665/862] Fix for starline authorization (#39674) --- homeassistant/components/starline/config_flow.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index 34415e9dca4..8650f158cfc 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -174,9 +174,11 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_authenticate_app(self, error=None): """Authenticate application.""" try: - self._app_code = self._auth.get_app_code(self._app_id, self._app_secret) - self._app_token = self._auth.get_app_token( - self._app_id, self._app_secret, self._app_code + self._app_code = await self.hass.async_add_executor_job( + self._auth.get_app_code, self._app_id, self._app_secret + ) + self._app_token = await self.hass.async_add_executor_job( + self._auth.get_app_token, self._app_id, self._app_secret, self._app_code ) return self._async_form_auth_user(error) except Exception as err: # pylint: disable=broad-except @@ -186,7 +188,8 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_authenticate_user(self, error=None): """Authenticate user.""" try: - state, data = self._auth.get_slid_user_token( + state, data = await self.hass.async_add_executor_job( + self._auth.get_slid_user_token, self._app_token, self._username, self._password, @@ -221,7 +224,9 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._slnet_token, self._slnet_token_expires, self._user_id, - ) = self._auth.get_user_id(self._user_slid) + ) = await self.hass.async_add_executor_job( + self._auth.get_user_id, self._user_slid + ) return self.async_create_entry( title=f"Application {self._app_id}", From d2b1918e9c67c10744eba1176bfc77dcf44719e0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 5 Sep 2020 21:09:14 +0200 Subject: [PATCH 666/862] Drop UNIT_ prefix for percentage constant (#39383) --- homeassistant/components/accuweather/const.py | 22 ++++----- homeassistant/components/acmeda/sensor.py | 4 +- homeassistant/components/adguard/sensor.py | 4 +- homeassistant/components/airly/sensor.py | 4 +- homeassistant/components/airvisual/sensor.py | 6 +-- .../components/ambient_station/__init__.py | 46 +++++++++---------- homeassistant/components/amcrest/sensor.py | 4 +- homeassistant/components/apcupsd/sensor.py | 18 ++++---- homeassistant/components/aqualogic/sensor.py | 4 +- homeassistant/components/arlo/sensor.py | 6 +-- homeassistant/components/atag/sensor.py | 4 +- homeassistant/components/august/sensor.py | 4 +- homeassistant/components/awair/const.py | 6 +-- .../components/beewi_smartclim/sensor.py | 6 +-- homeassistant/components/bloomsky/sensor.py | 6 +-- homeassistant/components/bme280/sensor.py | 4 +- homeassistant/components/bme680/sensor.py | 6 +-- .../components/bmw_connected_drive/sensor.py | 6 +-- homeassistant/components/bom/sensor.py | 4 +- homeassistant/components/broadlink/sensor.py | 4 +- homeassistant/components/brother/const.py | 38 +++++++-------- homeassistant/components/buienradar/sensor.py | 24 +++++----- homeassistant/components/canary/sensor.py | 6 +-- .../components/climate/device_trigger.py | 4 +- .../components/comfoconnect/sensor.py | 16 +++---- homeassistant/components/cups/sensor.py | 4 +- homeassistant/components/daikin/const.py | 6 +-- .../components/danfoss_air/sensor.py | 10 ++-- homeassistant/components/darksky/sensor.py | 32 ++++++------- homeassistant/components/deconz/sensor.py | 6 +-- homeassistant/components/demo/sensor.py | 4 +- homeassistant/components/dht/sensor.py | 4 +- homeassistant/components/dovado/sensor.py | 4 +- homeassistant/components/dyson/sensor.py | 4 +- homeassistant/components/ebox/sensor.py | 4 +- homeassistant/components/ecobee/sensor.py | 4 +- .../components/eight_sleep/sensor.py | 10 ++-- homeassistant/components/enocean/sensor.py | 4 +- .../components/epsonworkforce/sensor.py | 14 +++--- homeassistant/components/fibaro/sensor.py | 4 +- homeassistant/components/fitbit/sensor.py | 6 +-- homeassistant/components/foobot/sensor.py | 6 +-- .../components/garmin_connect/const.py | 38 +++++++-------- homeassistant/components/geniushub/sensor.py | 4 +- homeassistant/components/glances/const.py | 17 +++---- .../components/history_stats/sensor.py | 4 +- homeassistant/components/home_connect/api.py | 4 +- .../components/homekit/accessories.py | 4 +- .../components/homekit/type_humidifiers.py | 4 +- .../components/homekit/type_thermostats.py | 4 +- .../components/homekit_controller/sensor.py | 6 +-- homeassistant/components/homematic/sensor.py | 6 +-- .../components/homematicip_cloud/sensor.py | 8 ++-- homeassistant/components/htu21d/sensor.py | 4 +- homeassistant/components/hue/sensor.py | 4 +- .../components/humidifier/device_trigger.py | 6 +-- .../hunterdouglas_powerview/sensor.py | 4 +- homeassistant/components/icloud/sensor.py | 4 +- homeassistant/components/ios/sensor.py | 4 +- homeassistant/components/ipp/sensor.py | 4 +- homeassistant/components/isy994/const.py | 4 +- homeassistant/components/kaiterra/const.py | 4 +- homeassistant/components/konnected/sensor.py | 4 +- homeassistant/components/lacrosse/sensor.py | 4 +- homeassistant/components/lcn/const.py | 4 +- homeassistant/components/lightwave/sensor.py | 4 +- .../components/linux_battery/sensor.py | 9 +--- homeassistant/components/logi_circle/const.py | 6 +-- .../components/luftdaten/__init__.py | 4 +- .../components/meteo_france/const.py | 10 ++-- homeassistant/components/metoffice/sensor.py | 6 +-- homeassistant/components/microsoft/tts.py | 6 +-- homeassistant/components/miflora/sensor.py | 6 +-- homeassistant/components/mitemp_bt/sensor.py | 6 +-- .../components/mold_indicator/sensor.py | 6 +-- homeassistant/components/mychevy/sensor.py | 4 +- homeassistant/components/mysensors/sensor.py | 10 ++-- homeassistant/components/neato/sensor.py | 4 +- homeassistant/components/nest/sensor.py | 4 +- homeassistant/components/netatmo/sensor.py | 6 +-- homeassistant/components/netdata/sensor.py | 4 +- .../components/netgear_lte/sensor_types.py | 4 +- homeassistant/components/nexia/sensor.py | 8 ++-- .../components/nfandroidtv/notify.py | 12 ++--- .../components/nissan_leaf/sensor.py | 4 +- homeassistant/components/nut/const.py | 16 +++---- .../components/octoprint/__init__.py | 4 +- homeassistant/components/octoprint/sensor.py | 4 +- homeassistant/components/onewire/sensor.py | 14 +++--- .../components/opentherm_gw/const.py | 10 ++-- .../components/openweathermap/const.py | 6 +-- homeassistant/components/pi_hole/const.py | 4 +- homeassistant/components/plaato/sensor.py | 4 +- homeassistant/components/plant/__init__.py | 6 +-- homeassistant/components/plugwise/sensor.py | 10 ++-- homeassistant/components/point/sensor.py | 4 +- homeassistant/components/poolsense/sensor.py | 4 +- homeassistant/components/powerwall/sensor.py | 8 +--- .../components/prometheus/__init__.py | 4 +- homeassistant/components/qnap/sensor.py | 8 ++-- .../components/raincloud/__init__.py | 4 +- homeassistant/components/repetier/__init__.py | 4 +- homeassistant/components/rfxtrx/__init__.py | 6 +-- homeassistant/components/ring/sensor.py | 4 +- homeassistant/components/roomba/sensor.py | 4 +- homeassistant/components/sensehat/sensor.py | 4 +- homeassistant/components/shelly/sensor.py | 8 ++-- homeassistant/components/sht31/sensor.py | 4 +- homeassistant/components/skybeacon/sensor.py | 4 +- .../components/smartthings/sensor.py | 10 ++-- homeassistant/components/solaredge/const.py | 4 +- homeassistant/components/solarlog/const.py | 4 +- homeassistant/components/starline/sensor.py | 4 +- homeassistant/components/startca/sensor.py | 4 +- .../components/surepetcare/sensor.py | 4 +- homeassistant/components/syncthru/sensor.py | 6 +-- .../components/synology_dsm/const.py | 20 ++++---- .../components/systemmonitor/sensor.py | 10 ++-- homeassistant/components/tado/sensor.py | 6 +-- homeassistant/components/tahoma/sensor.py | 6 +-- .../components/tank_utility/sensor.py | 4 +- homeassistant/components/teksavvy/sensor.py | 4 +- .../components/tellduslive/sensor.py | 4 +- homeassistant/components/tellstick/sensor.py | 4 +- .../components/thinkingcleaner/sensor.py | 4 +- homeassistant/components/toon/const.py | 6 +-- homeassistant/components/tradfri/sensor.py | 4 +- .../trafikverket_weatherstation/sensor.py | 4 +- homeassistant/components/vallox/sensor.py | 6 +-- homeassistant/components/vera/sensor.py | 4 +- homeassistant/components/verisure/sensor.py | 4 +- homeassistant/components/vicare/sensor.py | 4 +- homeassistant/components/vilfo/const.py | 4 +- .../components/waterfurnace/sensor.py | 6 +-- .../components/wirelesstag/__init__.py | 4 +- homeassistant/components/withings/common.py | 6 +-- homeassistant/components/wled/sensor.py | 4 +- .../components/worxlandroid/sensor.py | 4 +- .../components/wunderground/sensor.py | 12 ++--- homeassistant/components/xbee/__init__.py | 4 +- .../components/xiaomi_aqara/sensor.py | 6 +-- .../components/xiaomi_miio/sensor.py | 4 +- homeassistant/components/zamg/sensor.py | 6 +-- homeassistant/components/zha/sensor.py | 6 +-- homeassistant/const.py | 2 +- tests/components/abode/test_sensor.py | 4 +- tests/components/accuweather/test_sensor.py | 12 ++--- tests/components/airly/test_sensor.py | 4 +- tests/components/arlo/test_sensor.py | 4 +- tests/components/august/test_sensor.py | 17 +++---- tests/components/awair/test_sensor.py | 4 +- tests/components/brother/test_sensor.py | 26 +++++------ tests/components/canary/test_sensor.py | 6 +-- tests/components/dyson/test_sensor.py | 8 ++-- tests/components/foobot/test_sensor.py | 6 +-- .../homekit/test_get_accessories.py | 4 +- tests/components/homekit/test_homekit.py | 4 +- .../homekit/test_type_humidifiers.py | 8 ++-- tests/components/homekit/test_type_lights.py | 17 +++---- tests/components/homekit/test_type_sensors.py | 4 +- .../homematicip_cloud/test_sensor.py | 8 ++-- tests/components/influxdb/test_init.py | 6 +-- tests/components/ipp/test_sensor.py | 12 ++--- tests/components/min_max/test_sensor.py | 4 +- tests/components/mobile_app/test_entity.py | 12 ++--- .../components/mold_indicator/test_sensor.py | 18 ++++---- tests/components/nexia/test_sensor.py | 8 ++-- tests/components/nut/test_sensor.py | 12 ++--- tests/components/powerwall/test_sensor.py | 4 +- tests/components/rflink/test_sensor.py | 6 +-- tests/components/rfxtrx/test_sensor.py | 20 ++++---- .../sensor/test_device_condition.py | 6 +-- .../components/sensor/test_device_trigger.py | 6 +-- tests/components/smartthings/test_sensor.py | 4 +- tests/components/spaceapi/test_init.py | 6 +-- tests/components/startca/test_sensor.py | 6 +-- tests/components/teksavvy/test_sensor.py | 6 +-- tests/components/vera/test_sensor.py | 4 +- tests/components/wled/test_sensor.py | 4 +- tests/components/zha/test_sensor.py | 4 +- tests/components/zwave/test_sensor.py | 4 +- tests/helpers/test_entity_platform.py | 6 +-- .../custom_components/test/sensor.py | 6 +-- 183 files changed, 639 insertions(+), 661 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index b189a776750..e572feafcf8 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -5,12 +5,12 @@ from homeassistant.const import ( LENGTH_FEET, LENGTH_INCHES, LENGTH_METERS, + PERCENTAGE, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_HOURS, - UNIT_PERCENTAGE, UV_INDEX, VOLUME_CUBIC_METERS, ) @@ -53,15 +53,15 @@ FORECAST_SENSOR_TYPES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:weather-cloudy", ATTR_LABEL: "Cloud Cover Day", - ATTR_UNIT_METRIC: UNIT_PERCENTAGE, - ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE, + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, }, "CloudCoverNight": { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:weather-cloudy", ATTR_LABEL: "Cloud Cover Night", - ATTR_UNIT_METRIC: UNIT_PERCENTAGE, - ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE, + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, }, "Grass": { ATTR_DEVICE_CLASS: None, @@ -130,15 +130,15 @@ FORECAST_SENSOR_TYPES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:weather-lightning", ATTR_LABEL: "Thunderstorm Probability Day", - ATTR_UNIT_METRIC: UNIT_PERCENTAGE, - ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE, + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, }, "ThunderstormProbabilityNight": { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:weather-lightning", ATTR_LABEL: "Thunderstorm Probability Night", - ATTR_UNIT_METRIC: UNIT_PERCENTAGE, - ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE, + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, }, "Tree": { ATTR_DEVICE_CLASS: None, @@ -210,8 +210,8 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:weather-cloudy", ATTR_LABEL: "Cloud Cover", - ATTR_UNIT_METRIC: UNIT_PERCENTAGE, - ATTR_UNIT_IMPERIAL: UNIT_PERCENTAGE, + ATTR_UNIT_METRIC: PERCENTAGE, + ATTR_UNIT_IMPERIAL: PERCENTAGE, }, "DewPoint": { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index e549160fbdd..f427548ab94 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -1,5 +1,5 @@ """Support for Acmeda Roller Blind Batteries.""" -from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -33,7 +33,7 @@ class AcmedaBattery(AcmedaBase): """Representation of a Acmeda cover device.""" device_class = DEVICE_CLASS_BATTERY - unit_of_measurement = UNIT_PERCENTAGE + unit_of_measurement = PERCENTAGE @property def name(self): diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 5abff10739a..10eec606028 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.adguard.const import ( DOMAIN, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TIME_MILLISECONDS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType @@ -134,7 +134,7 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): "AdGuard DNS Queries Blocked Ratio", "mdi:magnify-close", "blocked_percentage", - UNIT_PERCENTAGE, + PERCENTAGE, ) async def _adguard_update(self) -> None: diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 8d016c4e60b..d4f472dfca8 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -7,9 +7,9 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, PRESSURE_HPA, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -42,7 +42,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_ICON: None, ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_API_PRESSURE: { ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 0fbb9ec9b38..895ffa494a4 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -15,8 +15,8 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback @@ -57,8 +57,8 @@ GEOGRAPHY_SENSORS = [ GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."} NODE_PRO_SENSORS = [ - (SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE), - (SENSOR_KIND_HUMIDITY, "Humidity", DEVICE_CLASS_HUMIDITY, UNIT_PERCENTAGE), + (SENSOR_KIND_BATTERY_LEVEL, "Battery", DEVICE_CLASS_BATTERY, PERCENTAGE), + (SENSOR_KIND_HUMIDITY, "Humidity", DEVICE_CLASS_HUMIDITY, PERCENTAGE), (SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS), ] diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index b3f4aeec3bd..9428449dc75 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -15,10 +15,10 @@ from homeassistant.const import ( CONF_API_KEY, DEGREE, EVENT_HOMEASSISTANT_STOP, + PERCENTAGE, POWER_WATT, SPEED_MILES_PER_HOUR, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -160,18 +160,18 @@ SENSOR_TYPES = { TYPE_EVENTRAININ: ("Event Rain", "in", TYPE_SENSOR, None), TYPE_FEELSLIKE: ("Feels Like", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", TYPE_SENSOR, None), - TYPE_HUMIDITY10: ("Humidity 10", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY1: ("Humidity 1", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY2: ("Humidity 2", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY3: ("Humidity 3", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY4: ("Humidity 4", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY5: ("Humidity 5", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY6: ("Humidity 6", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY7: ("Humidity 7", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY8: ("Humidity 8", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY9: ("Humidity 9", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITY: ("Humidity", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_HUMIDITYIN: ("Humidity In", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY10: ("Humidity 10", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY1: ("Humidity 1", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY2: ("Humidity 2", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY3: ("Humidity 3", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY4: ("Humidity 4", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY5: ("Humidity 5", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY6: ("Humidity 6", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY7: ("Humidity 7", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY8: ("Humidity 8", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY9: ("Humidity 9", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITY: ("Humidity", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_HUMIDITYIN: ("Humidity In", PERCENTAGE, TYPE_SENSOR, "humidity"), TYPE_LASTRAIN: ("Last Rain", None, TYPE_SENSOR, "timestamp"), TYPE_MAXDAILYGUST: ("Max Gust", SPEED_MILES_PER_HOUR, TYPE_SENSOR, None), TYPE_MONTHLYRAININ: ("Monthly Rain", "in", TYPE_SENSOR, None), @@ -185,16 +185,16 @@ SENSOR_TYPES = { TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, "connectivity"), TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, "connectivity"), TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, "connectivity"), - TYPE_SOILHUM10: ("Soil Humidity 10", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM1: ("Soil Humidity 1", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM2: ("Soil Humidity 2", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM3: ("Soil Humidity 3", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM4: ("Soil Humidity 4", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM5: ("Soil Humidity 5", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM6: ("Soil Humidity 6", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM7: ("Soil Humidity 7", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM8: ("Soil Humidity 8", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), - TYPE_SOILHUM9: ("Soil Humidity 9", UNIT_PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM10: ("Soil Humidity 10", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM1: ("Soil Humidity 1", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM2: ("Soil Humidity 2", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM3: ("Soil Humidity 3", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM4: ("Soil Humidity 4", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM5: ("Soil Humidity 5", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM6: ("Soil Humidity 6", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM7: ("Soil Humidity 7", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM8: ("Soil Humidity 8", PERCENTAGE, TYPE_SENSOR, "humidity"), + TYPE_SOILHUM9: ("Soil Humidity 9", PERCENTAGE, TYPE_SENSOR, "humidity"), TYPE_SOILTEMP10F: ("Soil Temp 10", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), TYPE_SOILTEMP1F: ("Soil Temp 1", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), TYPE_SOILTEMP2F: ("Soil Temp 2", TEMP_FAHRENHEIT, TYPE_SENSOR, "temperature"), diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index 18abf28bdd5..44ebcfcdb95 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -4,7 +4,7 @@ import logging from amcrest import AmcrestError -from homeassistant.const import CONF_NAME, CONF_SENSORS, UNIT_PERCENTAGE +from homeassistant.const import CONF_NAME, CONF_SENSORS, PERCENTAGE from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -20,7 +20,7 @@ SENSOR_SDCARD = "sdcard" # Sensor types are defined like: Name, units, icon SENSORS = { SENSOR_PTZ_PRESET: ["PTZ Preset", None, "mdi:camera-iris"], - SENSOR_SDCARD: ["SD Used", UNIT_PERCENTAGE, "mdi:sd"], + SENSOR_SDCARD: ["SD Used", PERCENTAGE, "mdi:sd"], } diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 26eb86c7e64..55e6d8c7a56 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -10,11 +10,11 @@ from homeassistant.const import ( ELECTRICAL_CURRENT_AMPERE, ELECTRICAL_VOLT_AMPERE, FREQUENCY_HERTZ, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, TIME_MINUTES, TIME_SECONDS, - UNIT_PERCENTAGE, VOLT, ) import homeassistant.helpers.config_validation as cv @@ -34,7 +34,7 @@ SENSOR_TYPES = { "battdate": ["Battery Replaced", "", "mdi:calendar-clock"], "battstat": ["Battery Status", "", "mdi:information-outline"], "battv": ["Battery Voltage", VOLT, "mdi:flash"], - "bcharge": ["Battery", UNIT_PERCENTAGE, "mdi:battery"], + "bcharge": ["Battery", PERCENTAGE, "mdi:battery"], "cable": ["Cable Type", "", "mdi:ethernet-cable"], "cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline"], "date": ["Status Date", "", "mdi:calendar-clock"], @@ -48,20 +48,20 @@ SENSOR_TYPES = { "firmware": ["Firmware Version", "", "mdi:information-outline"], "hitrans": ["Transfer High", VOLT, "mdi:flash"], "hostname": ["Hostname", "", "mdi:information-outline"], - "humidity": ["Ambient Humidity", UNIT_PERCENTAGE, "mdi:water-percent"], + "humidity": ["Ambient Humidity", PERCENTAGE, "mdi:water-percent"], "itemp": ["Internal Temperature", TEMP_CELSIUS, "mdi:thermometer"], "lastxfer": ["Last Transfer", "", "mdi:transfer"], "linefail": ["Input Voltage Status", "", "mdi:information-outline"], "linefreq": ["Line Frequency", FREQUENCY_HERTZ, "mdi:information-outline"], "linev": ["Input Voltage", VOLT, "mdi:flash"], - "loadpct": ["Load", UNIT_PERCENTAGE, "mdi:gauge"], - "loadapnt": ["Load Apparent Power", UNIT_PERCENTAGE, "mdi:gauge"], + "loadpct": ["Load", PERCENTAGE, "mdi:gauge"], + "loadapnt": ["Load Apparent Power", PERCENTAGE, "mdi:gauge"], "lotrans": ["Transfer Low", VOLT, "mdi:flash"], "mandate": ["Manufacture Date", "", "mdi:calendar"], "masterupd": ["Master Update", "", "mdi:information-outline"], "maxlinev": ["Input Voltage High", VOLT, "mdi:flash"], "maxtime": ["Battery Timeout", "", "mdi:timer-off-outline"], - "mbattchg": ["Battery Shutdown", UNIT_PERCENTAGE, "mdi:battery-alert"], + "mbattchg": ["Battery Shutdown", PERCENTAGE, "mdi:battery-alert"], "minlinev": ["Input Voltage Low", VOLT, "mdi:flash"], "mintimel": ["Shutdown Time", "", "mdi:timer-outline"], "model": ["Model", "", "mdi:information-outline"], @@ -76,7 +76,7 @@ SENSOR_TYPES = { "reg1": ["Register 1 Fault", "", "mdi:information-outline"], "reg2": ["Register 2 Fault", "", "mdi:information-outline"], "reg3": ["Register 3 Fault", "", "mdi:information-outline"], - "retpct": ["Restore Requirement", UNIT_PERCENTAGE, "mdi:battery-alert"], + "retpct": ["Restore Requirement", PERCENTAGE, "mdi:battery-alert"], "selftest": ["Last Self Test", "", "mdi:calendar-clock"], "sense": ["Sensitivity", "", "mdi:information-outline"], "serialno": ["Serial Number", "", "mdi:information-outline"], @@ -98,14 +98,14 @@ SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS} INFERRED_UNITS = { " Minutes": TIME_MINUTES, " Seconds": TIME_SECONDS, - " Percent": UNIT_PERCENTAGE, + " Percent": PERCENTAGE, " Volts": VOLT, " Ampere": ELECTRICAL_CURRENT_AMPERE, " Volt-Ampere": ELECTRICAL_VOLT_AMPERE, " Watts": POWER_WATT, " Hz": FREQUENCY_HERTZ, " C": TEMP_CELSIUS, - " Percent Load Capacity": UNIT_PERCENTAGE, + " Percent Load Capacity": PERCENTAGE, } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index a53a8c1d348..138a4ecc8c8 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -6,10 +6,10 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -20,7 +20,7 @@ from . import DOMAIN, UPDATE_TOPIC _LOGGER = logging.getLogger(__name__) TEMP_UNITS = [TEMP_CELSIUS, TEMP_FAHRENHEIT] -PERCENT_UNITS = [UNIT_PERCENTAGE, UNIT_PERCENTAGE] +PERCENT_UNITS = [PERCENTAGE, PERCENTAGE] SALT_UNITS = ["g/L", "PPM"] WATT_UNITS = [POWER_WATT, POWER_WATT] NO_UNITS = [None, None] diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index 9942ce687f4..d5d583de22c 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -10,8 +10,8 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -28,10 +28,10 @@ SENSOR_TYPES = { "last_capture": ["Last", None, "run-fast"], "total_cameras": ["Arlo Cameras", None, "video"], "captured_today": ["Captured Today", None, "file-video"], - "battery_level": ["Battery Level", UNIT_PERCENTAGE, "battery-50"], + "battery_level": ["Battery Level", PERCENTAGE, "battery-50"], "signal_strength": ["Signal Strength", None, "signal"], "temperature": ["Temperature", TEMP_CELSIUS, "thermometer"], - "humidity": ["Humidity", UNIT_PERCENTAGE, "water-percent"], + "humidity": ["Humidity", PERCENTAGE, "water-percent"], "air_quality": ["Air Quality", CONCENTRATION_PARTS_PER_MILLION, "biohazard"], } diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index 1d647eb4764..d6abe16ffdb 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -2,11 +2,11 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, PRESSURE_BAR, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_HOURS, - UNIT_PERCENTAGE, ) from . import DOMAIN, AtagEntity @@ -67,7 +67,7 @@ class AtagSensor(AtagEntity): PRESSURE_BAR, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, + PERCENTAGE, TIME_HOURS, ]: return self.coordinator.data[self._id].measure diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index c8f2704da8d..c3f5f05ceef 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -4,7 +4,7 @@ import logging from august.activity import ActivityType from homeassistant.components.sensor import DEVICE_CLASS_BATTERY -from homeassistant.const import ATTR_ENTITY_PICTURE, UNIT_PERCENTAGE +from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import async_get_registry @@ -244,7 +244,7 @@ class AugustBatterySensor(AugustEntityMixin, Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def device_class(self): diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 5735078eee5..e3c2a176119 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -14,8 +14,8 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) API_CO2 = "carbon_dioxide" @@ -49,14 +49,14 @@ SENSOR_TYPES = { API_SCORE: { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:blur", - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_LABEL: "Awair score", ATTR_UNIQUE_ID: "score", # matches legacy format }, API_HUMID: { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_ICON: None, - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_LABEL: "Humidity", ATTR_UNIQUE_ID: "HUMID", # matches legacy format }, diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index b4dcc289a7b..b155a1e06be 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -11,8 +11,8 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -25,8 +25,8 @@ DEFAULT_NAME = "BeeWi SmartClim" # Sensor config SENSOR_TYPES = [ [DEVICE_CLASS_TEMPERATURE, "Temperature", TEMP_CELSIUS], - [DEVICE_CLASS_HUMIDITY, "Humidity", UNIT_PERCENTAGE], - [DEVICE_CLASS_BATTERY, "Battery", UNIT_PERCENTAGE], + [DEVICE_CLASS_HUMIDITY, "Humidity", PERCENTAGE], + [DEVICE_CLASS_BATTERY, "Battery", PERCENTAGE], ] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 0a2c19a8cd8..0ddeec6a577 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -6,9 +6,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, + PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -30,7 +30,7 @@ SENSOR_TYPES = [ # Sensor units - these do not currently align with the API documentation SENSOR_UNITS_IMPERIAL = { "Temperature": TEMP_FAHRENHEIT, - "Humidity": UNIT_PERCENTAGE, + "Humidity": PERCENTAGE, "Pressure": "inHg", "Luminance": "cd/m²", "Voltage": "mV", @@ -39,7 +39,7 @@ SENSOR_UNITS_IMPERIAL = { # Metric units SENSOR_UNITS_METRIC = { "Temperature": TEMP_CELSIUS, - "Humidity": UNIT_PERCENTAGE, + "Humidity": PERCENTAGE, "Pressure": "mbar", "Luminance": "cd/m²", "Voltage": "mV", diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index 893ddbf54e9..dd5ed905fe2 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -11,8 +11,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, + PERCENTAGE, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -49,7 +49,7 @@ SENSOR_HUMID = "humidity" SENSOR_PRESS = "pressure" SENSOR_TYPES = { SENSOR_TEMP: ["Temperature", None], - SENSOR_HUMID: ["Humidity", UNIT_PERCENTAGE], + SENSOR_HUMID: ["Humidity", PERCENTAGE], SENSOR_PRESS: ["Pressure", "mb"], } DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS] diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 2d274c077a4..e412b410935 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -11,8 +11,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, + PERCENTAGE, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -55,10 +55,10 @@ SENSOR_GAS = "gas" SENSOR_AQ = "airquality" SENSOR_TYPES = { SENSOR_TEMP: ["Temperature", None], - SENSOR_HUMID: ["Humidity", UNIT_PERCENTAGE], + SENSOR_HUMID: ["Humidity", PERCENTAGE], SENSOR_PRESS: ["Pressure", "mb"], SENSOR_GAS: ["Gas Resistance", "Ohms"], - SENSOR_AQ: ["Air Quality", UNIT_PERCENTAGE], + SENSOR_AQ: ["Air Quality", PERCENTAGE], } DEFAULT_MONITORED = [SENSOR_TEMP, SENSOR_HUMID, SENSOR_PRESS, SENSOR_AQ] OVERSAMPLING_VALUES = {0, 1, 2, 4, 8, 16} diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index d7eec8b9479..995a6a6ef86 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -8,8 +8,8 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, + PERCENTAGE, TIME_HOURS, - UNIT_PERCENTAGE, VOLUME_GALLONS, VOLUME_LITERS, ) @@ -31,7 +31,7 @@ ATTR_TO_HA_METRIC = { "charging_time_remaining": ["mdi:update", TIME_HOURS], "charging_status": ["mdi:battery-charging", None], # No icon as this is dealt with directly as a special case in icon() - "charging_level_hv": [None, UNIT_PERCENTAGE], + "charging_level_hv": [None, PERCENTAGE], } ATTR_TO_HA_IMPERIAL = { @@ -44,7 +44,7 @@ ATTR_TO_HA_IMPERIAL = { "charging_time_remaining": ["mdi:update", TIME_HOURS], "charging_status": ["mdi:battery-charging", None], # No icon as this is dealt with directly as a special case in icon() - "charging_level_hv": [None, UNIT_PERCENTAGE], + "charging_level_hv": [None, PERCENTAGE], } diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index a32b36796de..56406b29e82 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -21,9 +21,9 @@ from homeassistant.const import ( CONF_NAME, LENGTH_KILOMETERS, LENGTH_METERS, + PERCENTAGE, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -71,7 +71,7 @@ SENSOR_TYPES = { "press_msl": ["Pressure msl", "msl"], "press_tend": ["Pressure Tend", None], "rain_trace": ["Rain Today", "mm"], - "rel_hum": ["Relative Humidity", UNIT_PERCENTAGE], + "rel_hum": ["Relative Humidity", PERCENTAGE], "sea_state": ["Sea State", None], "swell_dir_worded": ["Swell Direction", None], "swell_height": ["Swell Height", LENGTH_METERS], diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index def52a1bc29..af350329e8c 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, ) -from homeassistant.const import CONF_HOST, TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import CONF_HOST, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { "temperature": ("Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE), "air_quality": ("Air Quality", None, None), - "humidity": ("Humidity", UNIT_PERCENTAGE, DEVICE_CLASS_HUMIDITY), + "humidity": ("Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY), "light": ("Light", None, DEVICE_CLASS_ILLUMINANCE), "noise": ("Noise", None, None), } diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 98229f5c7a2..9aa0a4f4a00 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -1,5 +1,5 @@ """Constants for Brother integration.""" -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE ATTR_BELT_UNIT_REMAINING_LIFE = "belt_unit_remaining_life" ATTR_BLACK_DRUM_COUNTER = "black_drum_counter" @@ -75,92 +75,92 @@ SENSOR_TYPES = { ATTR_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_BLACK_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_CYAN_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_MAGENTA_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_YELLOW_DRUM_REMAINING_LIFE: { ATTR_ICON: "mdi:chart-donut", ATTR_LABEL: ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_BELT_UNIT_REMAINING_LIFE: { ATTR_ICON: "mdi:current-ac", ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_FUSER_REMAINING_LIFE: { ATTR_ICON: "mdi:water-outline", ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_LASER_REMAINING_LIFE: { ATTR_ICON: "mdi:spotlight-beam", ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_PF_KIT_1_REMAINING_LIFE: { ATTR_ICON: "mdi:printer-3d", ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_PF_KIT_MP_REMAINING_LIFE: { ATTR_ICON: "mdi:printer-3d", ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_BLACK_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_CYAN_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_MAGENTA_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_YELLOW_TONER_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_BLACK_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_CYAN_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_MAGENTA_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_YELLOW_INK_REMAINING: { ATTR_ICON: "mdi:printer-3d-nozzle", ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, }, ATTR_UPTIME: {ATTR_ICON: None, ATTR_LABEL: ATTR_UPTIME.title(), ATTR_UNIT: None}, } diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 47bacfe2247..ba0063ebb85 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -30,10 +30,10 @@ from homeassistant.const import ( DEGREE, IRRADIATION_WATTS_PER_SQUARE_METER, LENGTH_KILOMETERS, + PERCENTAGE, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, TIME_HOURS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -71,7 +71,7 @@ SENSOR_TYPES = { "symbol": ["Symbol", None, None], # new in json api (>1.0.0): "feeltemperature": ["Feel temperature", TEMP_CELSIUS, "mdi:thermometer"], - "humidity": ["Humidity", UNIT_PERCENTAGE, "mdi:water-percent"], + "humidity": ["Humidity", PERCENTAGE, "mdi:water-percent"], "temperature": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"], "groundtemperature": ["Ground temperature", TEMP_CELSIUS, "mdi:thermometer"], "windspeed": ["Wind speed", SPEED_KILOMETERS_PER_HOUR, "mdi:weather-windy"], @@ -124,16 +124,16 @@ SENSOR_TYPES = { "maxrain_3d": ["Maximum rain 3d", "mm", "mdi:weather-pouring"], "maxrain_4d": ["Maximum rain 4d", "mm", "mdi:weather-pouring"], "maxrain_5d": ["Maximum rain 5d", "mm", "mdi:weather-pouring"], - "rainchance_1d": ["Rainchance 1d", UNIT_PERCENTAGE, "mdi:weather-pouring"], - "rainchance_2d": ["Rainchance 2d", UNIT_PERCENTAGE, "mdi:weather-pouring"], - "rainchance_3d": ["Rainchance 3d", UNIT_PERCENTAGE, "mdi:weather-pouring"], - "rainchance_4d": ["Rainchance 4d", UNIT_PERCENTAGE, "mdi:weather-pouring"], - "rainchance_5d": ["Rainchance 5d", UNIT_PERCENTAGE, "mdi:weather-pouring"], - "sunchance_1d": ["Sunchance 1d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"], - "sunchance_2d": ["Sunchance 2d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"], - "sunchance_3d": ["Sunchance 3d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"], - "sunchance_4d": ["Sunchance 4d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"], - "sunchance_5d": ["Sunchance 5d", UNIT_PERCENTAGE, "mdi:weather-partly-cloudy"], + "rainchance_1d": ["Rainchance 1d", PERCENTAGE, "mdi:weather-pouring"], + "rainchance_2d": ["Rainchance 2d", PERCENTAGE, "mdi:weather-pouring"], + "rainchance_3d": ["Rainchance 3d", PERCENTAGE, "mdi:weather-pouring"], + "rainchance_4d": ["Rainchance 4d", PERCENTAGE, "mdi:weather-pouring"], + "rainchance_5d": ["Rainchance 5d", PERCENTAGE, "mdi:weather-pouring"], + "sunchance_1d": ["Sunchance 1d", PERCENTAGE, "mdi:weather-partly-cloudy"], + "sunchance_2d": ["Sunchance 2d", PERCENTAGE, "mdi:weather-partly-cloudy"], + "sunchance_3d": ["Sunchance 3d", PERCENTAGE, "mdi:weather-partly-cloudy"], + "sunchance_4d": ["Sunchance 4d", PERCENTAGE, "mdi:weather-partly-cloudy"], + "sunchance_5d": ["Sunchance 5d", PERCENTAGE, "mdi:weather-partly-cloudy"], "windforce_1d": ["Wind force 1d", "Bft", "mdi:weather-windy"], "windforce_2d": ["Wind force 2d", "Bft", "mdi:weather-windy"], "windforce_3d": ["Wind force 3d", "Bft", "mdi:weather-windy"], diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 0be5171af48..5f1b1fe906b 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,7 +1,7 @@ """Support for Canary sensors.""" from canary.api import SensorType -from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -21,10 +21,10 @@ CANARY_FLEX = "Canary Flex" # sensor type name, unit_of_measurement, icon SENSOR_TYPES = [ ["temperature", TEMP_CELSIUS, "mdi:thermometer", [CANARY_PRO]], - ["humidity", UNIT_PERCENTAGE, "mdi:water-percent", [CANARY_PRO]], + ["humidity", PERCENTAGE, "mdi:water-percent", [CANARY_PRO]], ["air_quality", None, "mdi:weather-windy", [CANARY_PRO]], ["wifi", "dBm", "mdi:wifi", [CANARY_FLEX]], - ["battery", UNIT_PERCENTAGE, "mdi:battery-50", [CANARY_FLEX]], + ["battery", PERCENTAGE, "mdi:battery-50", [CANARY_FLEX]], ] STATE_AIR_QUALITY_NORMAL = "normal" diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 1dabe65e6d0..84a7c35162a 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_FOR, CONF_PLATFORM, CONF_TYPE, - UNIT_PERCENTAGE, + PERCENTAGE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry @@ -178,7 +178,7 @@ async def async_get_trigger_capabilities(hass: HomeAssistant, config): if trigger_type == "current_temperature_changed": unit_of_measurement = hass.config.units.temperature_unit else: - unit_of_measurement = UNIT_PERCENTAGE + unit_of_measurement = PERCENTAGE return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index cea09e97dba..6ec92477555 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -29,11 +29,11 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, TIME_DAYS, TIME_HOURS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -79,7 +79,7 @@ SENSOR_TYPES = { ATTR_CURRENT_HUMIDITY: { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_LABEL: "Inside Humidity", - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_ICON: "mdi:water-percent", ATTR_ID: SENSOR_HUMIDITY_EXTRACT, }, @@ -94,7 +94,7 @@ SENSOR_TYPES = { ATTR_OUTSIDE_HUMIDITY: { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_LABEL: "Outside Humidity", - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_ICON: "mdi:water-percent", ATTR_ID: SENSOR_HUMIDITY_OUTDOOR, }, @@ -109,7 +109,7 @@ SENSOR_TYPES = { ATTR_SUPPLY_HUMIDITY: { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_LABEL: "Supply Humidity", - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_ICON: "mdi:water-percent", ATTR_ID: SENSOR_HUMIDITY_SUPPLY, }, @@ -123,7 +123,7 @@ SENSOR_TYPES = { ATTR_SUPPLY_FAN_DUTY: { ATTR_DEVICE_CLASS: None, ATTR_LABEL: "Supply Fan Duty", - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_ICON: "mdi:fan", ATTR_ID: SENSOR_FAN_SUPPLY_DUTY, }, @@ -137,7 +137,7 @@ SENSOR_TYPES = { ATTR_EXHAUST_FAN_DUTY: { ATTR_DEVICE_CLASS: None, ATTR_LABEL: "Exhaust Fan Duty", - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_ICON: "mdi:fan", ATTR_ID: SENSOR_FAN_EXHAUST_DUTY, }, @@ -152,7 +152,7 @@ SENSOR_TYPES = { ATTR_EXHAUST_HUMIDITY: { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_LABEL: "Exhaust Humidity", - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_ICON: "mdi:water-percent", ATTR_ID: SENSOR_HUMIDITY_EXHAUST, }, @@ -173,7 +173,7 @@ SENSOR_TYPES = { ATTR_BYPASS_STATE: { ATTR_DEVICE_CLASS: None, ATTR_LABEL: "Bypass State", - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_ICON: "mdi:camera-iris", ATTR_ID: SENSOR_BYPASS_STATE, }, diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index bc6cdbe8ba1..72d2aa62ae0 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -6,7 +6,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_HOST, CONF_PORT, UNIT_PERCENTAGE +from homeassistant.const import CONF_HOST, CONF_PORT, PERCENTAGE from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -268,7 +268,7 @@ class MarkerSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def device_state_attributes(self): diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index a28221dbcbf..6662954c1dc 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -9,9 +9,9 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, + PERCENTAGE, POWER_KILO_WATT, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) ATTR_TARGET_TEMPERATURE = "target_temperature" @@ -48,13 +48,13 @@ SENSOR_TYPES = { CONF_NAME: "Humidity", CONF_TYPE: SENSOR_TYPE_HUMIDITY, CONF_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - CONF_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ATTR_TARGET_HUMIDITY: { CONF_NAME: "Target Humidity", CONF_TYPE: SENSOR_TYPE_HUMIDITY, CONF_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - CONF_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ATTR_TOTAL_POWER: { CONF_NAME: "Total Power Consumption", diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index f03c74ae78b..251c9692021 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -7,8 +7,8 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.helpers.entity import Entity @@ -48,22 +48,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ], [ "Danfoss Air Remaining Filter", - UNIT_PERCENTAGE, + PERCENTAGE, ReadCommand.filterPercent, None, ], [ "Danfoss Air Humidity", - UNIT_PERCENTAGE, + PERCENTAGE, ReadCommand.humidity, DEVICE_CLASS_HUMIDITY, ], - ["Danfoss Air Fan Step", UNIT_PERCENTAGE, ReadCommand.fan_step, None], + ["Danfoss Air Fan Step", PERCENTAGE, ReadCommand.fan_step, None], ["Danfoss Air Exhaust Fan Speed", "RPM", ReadCommand.exhaust_fan_speed, None], ["Danfoss Air Supply Fan Speed", "RPM", ReadCommand.supply_fan_speed, None], [ "Danfoss Air Dial Battery", - UNIT_PERCENTAGE, + PERCENTAGE, ReadCommand.battery_percent, DEVICE_CLASS_BATTERY, ], diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index aee74179c45..e167f16b4e4 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -18,13 +18,13 @@ from homeassistant.const import ( DEGREE, LENGTH_CENTIMETERS, LENGTH_KILOMETERS, + PERCENTAGE, SPEED_KILOMETERS_PER_HOUR, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_HOURS, - UNIT_PERCENTAGE, UV_INDEX, ) import homeassistant.helpers.config_validation as cv @@ -119,11 +119,11 @@ SENSOR_TYPES = { ], "precip_probability": [ "Precip Probability", - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, + PERCENTAGE, + PERCENTAGE, + PERCENTAGE, + PERCENTAGE, + PERCENTAGE, "mdi:water-percent", ["currently", "minutely", "hourly", "daily"], ], @@ -199,21 +199,21 @@ SENSOR_TYPES = { ], "cloud_cover": [ "Cloud Coverage", - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, + PERCENTAGE, + PERCENTAGE, + PERCENTAGE, + PERCENTAGE, + PERCENTAGE, "mdi:weather-partly-cloudy", ["currently", "hourly", "daily"], ], "humidity": [ "Humidity", - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, - UNIT_PERCENTAGE, + PERCENTAGE, + PERCENTAGE, + PERCENTAGE, + PERCENTAGE, + PERCENTAGE, "mdi:water-percent", ["currently", "hourly", "daily"], ], diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 330c262110a..4ebed981e2d 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -22,10 +22,10 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, + PERCENTAGE, POWER_WATT, PRESSURE_HPA, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( @@ -59,7 +59,7 @@ ICON = { UNIT_OF_MEASUREMENT = { Consumption: ENERGY_KILO_WATT_HOUR, - Humidity: UNIT_PERCENTAGE, + Humidity: PERCENTAGE, LightLevel: "lx", Power: POWER_WATT, Pressure: PRESSURE_HPA, @@ -234,7 +234,7 @@ class DeconzBattery(DeconzDevice): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def device_state_attributes(self): diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 6805ebb5b56..99aadf356d7 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -3,8 +3,8 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.helpers.entity import Entity @@ -28,7 +28,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Outside Humidity", 54, DEVICE_CLASS_HUMIDITY, - UNIT_PERCENTAGE, + PERCENTAGE, None, ), ] diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 6f3e58d4ad4..57e12d03ffe 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -9,8 +9,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, + PERCENTAGE, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -33,7 +33,7 @@ SENSOR_TEMPERATURE = "temperature" SENSOR_HUMIDITY = "humidity" SENSOR_TYPES = { SENSOR_TEMPERATURE: ["Temperature", None], - SENSOR_HUMIDITY: ["Humidity", UNIT_PERCENTAGE], + SENSOR_HUMIDITY: ["Humidity", PERCENTAGE], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index 8328df8bd7f..0de159861d6 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -6,7 +6,7 @@ import re import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_SENSORS, DATA_GIGABYTES, UNIT_PERCENTAGE +from homeassistant.const import CONF_SENSORS, DATA_GIGABYTES, PERCENTAGE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -27,7 +27,7 @@ SENSORS = { SENSOR_SIGNAL: ( "signal strength", "Signal Strength", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:signal", ), SENSOR_SMS_UNREAD: ("sms unread", "SMS unread", "", "mdi:message-text-outline"), diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index 7d73e1d43b3..98c69a7a7db 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -4,7 +4,7 @@ import logging from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink -from homeassistant.const import STATE_OFF, TEMP_CELSIUS, TIME_HOURS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, STATE_OFF, TEMP_CELSIUS, TIME_HOURS from homeassistant.helpers.entity import Entity from . import DYSON_DEVICES @@ -13,7 +13,7 @@ SENSOR_UNITS = { "air_quality": None, "dust": None, "filter_life": TIME_HOURS, - "humidity": UNIT_PERCENTAGE, + "humidity": PERCENTAGE, } SENSOR_ICONS = { diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 7bbddb18e7b..aa6945bac99 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -17,8 +17,8 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, DATA_GIGABITS, + PERCENTAGE, TIME_DAYS, - UNIT_PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -36,7 +36,7 @@ SCAN_INTERVAL = timedelta(minutes=15) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { - "usage": ["Usage", UNIT_PERCENTAGE, "mdi:percent"], + "usage": ["Usage", PERCENTAGE, "mdi:percent"], "balance": ["Balance", PRICE, "mdi:cash-usd"], "limit": ["Data limit", DATA_GIGABITS, "mdi:download"], "days_left": ["Days left", TIME_DAYS, "mdi:calendar-today"], diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 4fd1a061cff..d9d6e74e3de 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -4,8 +4,8 @@ from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.helpers.entity import Entity @@ -13,7 +13,7 @@ from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], - "humidity": ["Humidity", UNIT_PERCENTAGE], + "humidity": ["Humidity", PERCENTAGE], } diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index ff6dff85aca..824ea210d69 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -1,7 +1,7 @@ """Support for Eight Sleep sensors.""" import logging -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT from . import ( CONF_SENSORS, @@ -20,9 +20,9 @@ ATTR_AVG_RESP_RATE = "Average Respiratory Rate" ATTR_HEART_RATE = "Heart Rate" ATTR_AVG_HEART_RATE = "Average Heart Rate" ATTR_SLEEP_DUR = "Time Slept" -ATTR_LIGHT_PERC = f"Light Sleep {UNIT_PERCENTAGE}" -ATTR_DEEP_PERC = f"Deep Sleep {UNIT_PERCENTAGE}" -ATTR_REM_PERC = f"REM Sleep {UNIT_PERCENTAGE}" +ATTR_LIGHT_PERC = f"Light Sleep {PERCENTAGE}" +ATTR_DEEP_PERC = f"Deep Sleep {PERCENTAGE}" +ATTR_REM_PERC = f"REM Sleep {PERCENTAGE}" ATTR_TNT = "Tosses & Turns" ATTR_SLEEP_STAGE = "Sleep Stage" ATTR_TARGET_HEAT = "Target Heating Level" @@ -102,7 +102,7 @@ class EightHeatSensor(EightSleepHeatEntity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return UNIT_PERCENTAGE + return PERCENTAGE async def async_update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 07d06824365..047d0a03986 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -11,11 +11,11 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, POWER_WATT, STATE_CLOSED, STATE_OPEN, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -39,7 +39,7 @@ SENSOR_TYPE_WINDOWHANDLE = "windowhandle" SENSOR_TYPES = { SENSOR_TYPE_HUMIDITY: { "name": "Humidity", - "unit": UNIT_PERCENTAGE, + "unit": PERCENTAGE, "icon": "mdi:water-percent", "class": DEVICE_CLASS_HUMIDITY, }, diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index b2164325547..cee3d00242a 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -6,19 +6,19 @@ from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_HOST, CONF_MONITORED_CONDITIONS, UNIT_PERCENTAGE +from homeassistant.const import CONF_HOST, CONF_MONITORED_CONDITIONS, PERCENTAGE from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) MONITORED_CONDITIONS = { - "black": ["Ink level Black", UNIT_PERCENTAGE, "mdi:water"], - "photoblack": ["Ink level Photoblack", UNIT_PERCENTAGE, "mdi:water"], - "magenta": ["Ink level Magenta", UNIT_PERCENTAGE, "mdi:water"], - "cyan": ["Ink level Cyan", UNIT_PERCENTAGE, "mdi:water"], - "yellow": ["Ink level Yellow", UNIT_PERCENTAGE, "mdi:water"], - "clean": ["Cleaning level", UNIT_PERCENTAGE, "mdi:water"], + "black": ["Ink level Black", PERCENTAGE, "mdi:water"], + "photoblack": ["Ink level Photoblack", PERCENTAGE, "mdi:water"], + "magenta": ["Ink level Magenta", PERCENTAGE, "mdi:water"], + "cyan": ["Ink level Cyan", PERCENTAGE, "mdi:water"], + "yellow": ["Ink level Yellow", PERCENTAGE, "mdi:water"], + "clean": ["Cleaning level", PERCENTAGE, "mdi:water"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 68a39431a98..e9e7265f917 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -7,9 +7,9 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.helpers.entity import Entity @@ -31,7 +31,7 @@ SENSOR_TYPES = { "CO2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:cloud", None], "com.fibaro.humiditySensor": [ "Humidity", - UNIT_PERCENTAGE, + PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, ], diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index d3f33832369..56afae1e9a7 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -19,9 +19,9 @@ from homeassistant.const import ( LENGTH_FEET, MASS_KILOGRAMS, MASS_MILLIGRAMS, + PERCENTAGE, TIME_MILLISECONDS, TIME_MINUTES, - UNIT_PERCENTAGE, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -97,11 +97,11 @@ FITBIT_RESOURCES_LIST = { ], "activities/tracker/steps": ["Tracker Steps", "steps", "walk"], "body/bmi": ["BMI", "BMI", "human"], - "body/fat": ["Body Fat", UNIT_PERCENTAGE, "human"], + "body/fat": ["Body Fat", PERCENTAGE, "human"], "body/weight": ["Weight", "", "human"], "devices/battery": ["Battery", None, None], "sleep/awakeningsCount": ["Awakenings Count", "times awaken", "sleep"], - "sleep/efficiency": ["Sleep Efficiency", UNIT_PERCENTAGE, "sleep"], + "sleep/efficiency": ["Sleep Efficiency", PERCENTAGE, "sleep"], "sleep/minutesAfterWakeup": ["Minutes After Wakeup", TIME_MINUTES, "sleep"], "sleep/minutesAsleep": ["Sleep Minutes Asleep", TIME_MINUTES, "sleep"], "sleep/minutesAwake": ["Sleep Minutes Awake", TIME_MINUTES, "sleep"], diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 3e71963a009..9ca265ba9ef 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -15,9 +15,9 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, CONF_TOKEN, CONF_USERNAME, + PERCENTAGE, TEMP_CELSIUS, TIME_SECONDS, - UNIT_PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -38,14 +38,14 @@ SENSOR_TYPES = { "time": [ATTR_TIME, TIME_SECONDS], "pm": [ATTR_PM2_5, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "mdi:cloud"], "tmp": [ATTR_TEMPERATURE, TEMP_CELSIUS, "mdi:thermometer"], - "hum": [ATTR_HUMIDITY, UNIT_PERCENTAGE, "mdi:water-percent"], + "hum": [ATTR_HUMIDITY, PERCENTAGE, "mdi:water-percent"], "co2": [ATTR_CARBON_DIOXIDE, CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2"], "voc": [ ATTR_VOLATILE_ORGANIC_COMPOUNDS, CONCENTRATION_PARTS_PER_BILLION, "mdi:cloud", ], - "allpollu": [ATTR_FOOBOT_INDEX, UNIT_PERCENTAGE, "mdi:percent"], + "allpollu": [ATTR_FOOBOT_INDEX, PERCENTAGE, "mdi:percent"], } SCAN_INTERVAL = timedelta(minutes=10) diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 3c30340fc61..77db3359a71 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -3,8 +3,8 @@ from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, LENGTH_METERS, MASS_KILOGRAMS, + PERCENTAGE, TIME_MINUTES, - UNIT_PERCENTAGE, ) DOMAIN = "garmin_connect" @@ -191,49 +191,49 @@ GARMIN_ENTITY_LIST = { ], "stressPercentage": [ "Stress Percentage", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:flash-alert", None, False, ], "restStressPercentage": [ "Rest Stress Percentage", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:flash-alert", None, False, ], "activityStressPercentage": [ "Activity Stress Percentage", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:flash-alert", None, False, ], "uncategorizedStressPercentage": [ "Uncat. Stress Percentage", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:flash-alert", None, False, ], "lowStressPercentage": [ "Low Stress Percentage", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:flash-alert", None, False, ], "mediumStressPercentage": [ "Medium Stress Percentage", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:flash-alert", None, False, ], "highStressPercentage": [ "High Stress Percentage", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:flash-alert", None, False, @@ -261,42 +261,42 @@ GARMIN_ENTITY_LIST = { ], "bodyBatteryChargedValue": [ "Body Battery Charged", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:battery-charging-100", None, True, ], "bodyBatteryDrainedValue": [ "Body Battery Drained", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:battery-alert-variant-outline", None, True, ], "bodyBatteryHighestValue": [ "Body Battery Highest", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:battery-heart", None, True, ], "bodyBatteryLowestValue": [ "Body Battery Lowest", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:battery-heart-outline", None, True, ], "bodyBatteryMostRecentValue": [ "Body Battery Most Recent", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:battery-positive", None, True, ], - "averageSpo2": ["Average SPO2", UNIT_PERCENTAGE, "mdi:diabetes", None, True], - "lowestSpo2": ["Lowest SPO2", UNIT_PERCENTAGE, "mdi:diabetes", None, True], - "latestSpo2": ["Latest SPO2", UNIT_PERCENTAGE, "mdi:diabetes", None, True], + "averageSpo2": ["Average SPO2", PERCENTAGE, "mdi:diabetes", None, True], + "lowestSpo2": ["Lowest SPO2", PERCENTAGE, "mdi:diabetes", None, True], + "latestSpo2": ["Latest SPO2", PERCENTAGE, "mdi:diabetes", None, True], "latestSpo2ReadingTimeLocal": [ "Latest SPO2 Time", "", @@ -306,7 +306,7 @@ GARMIN_ENTITY_LIST = { ], "averageMonitoringEnvironmentAltitude": [ "Average Altitude", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:image-filter-hdr", None, False, @@ -341,8 +341,8 @@ GARMIN_ENTITY_LIST = { ], "weight": ["Weight", MASS_KILOGRAMS, "mdi:weight-kilogram", None, False], "bmi": ["BMI", "", "mdi:food", None, False], - "bodyFat": ["Body Fat", UNIT_PERCENTAGE, "mdi:food", None, False], - "bodyWater": ["Body Water", UNIT_PERCENTAGE, "mdi:water-percent", None, False], + "bodyFat": ["Body Fat", PERCENTAGE, "mdi:food", None, False], + "bodyWater": ["Body Water", PERCENTAGE, "mdi:water-percent", None, False], "bodyMass": ["Body Mass", MASS_KILOGRAMS, "mdi:food", None, False], "muscleMass": ["Muscle Mass", MASS_KILOGRAMS, "mdi:dumbbell", None, False], "physiqueRating": ["Physique Rating", "", "mdi:numeric", None, False], diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 196cba7212e..7e4fe81fc77 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta from typing import Any, Dict -from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util @@ -77,7 +77,7 @@ class GeniusBattery(GeniusDevice): @property def unit_of_measurement(self) -> str: """Return the unit of measurement of the sensor.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def state(self) -> str: diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index d30dd87baf3..491d400411c 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -1,12 +1,7 @@ """Constants for Glances component.""" import sys -from homeassistant.const import ( - DATA_GIBIBYTES, - DATA_MEBIBYTES, - TEMP_CELSIUS, - UNIT_PERCENTAGE, -) +from homeassistant.const import DATA_GIBIBYTES, DATA_MEBIBYTES, PERCENTAGE, TEMP_CELSIUS DOMAIN = "glances" CONF_VERSION = "version" @@ -26,13 +21,13 @@ else: CPU_ICON = "mdi:cpu-32-bit" SENSOR_TYPES = { - "disk_use_percent": ["fs", "used percent", UNIT_PERCENTAGE, "mdi:harddisk"], + "disk_use_percent": ["fs", "used percent", PERCENTAGE, "mdi:harddisk"], "disk_use": ["fs", "used", DATA_GIBIBYTES, "mdi:harddisk"], "disk_free": ["fs", "free", DATA_GIBIBYTES, "mdi:harddisk"], - "memory_use_percent": ["mem", "RAM used percent", UNIT_PERCENTAGE, "mdi:memory"], + "memory_use_percent": ["mem", "RAM used percent", PERCENTAGE, "mdi:memory"], "memory_use": ["mem", "RAM used", DATA_MEBIBYTES, "mdi:memory"], "memory_free": ["mem", "RAM free", DATA_MEBIBYTES, "mdi:memory"], - "swap_use_percent": ["memswap", "Swap used percent", UNIT_PERCENTAGE, "mdi:memory"], + "swap_use_percent": ["memswap", "Swap used percent", PERCENTAGE, "mdi:memory"], "swap_use": ["memswap", "Swap used", DATA_GIBIBYTES, "mdi:memory"], "swap_free": ["memswap", "Swap free", DATA_GIBIBYTES, "mdi:memory"], "processor_load": ["load", "CPU load", "15 min", CPU_ICON], @@ -40,10 +35,10 @@ SENSOR_TYPES = { "process_total": ["processcount", "Total", "Count", CPU_ICON], "process_thread": ["processcount", "Thread", "Count", CPU_ICON], "process_sleeping": ["processcount", "Sleeping", "Count", CPU_ICON], - "cpu_use_percent": ["cpu", "CPU used", UNIT_PERCENTAGE, CPU_ICON], + "cpu_use_percent": ["cpu", "CPU used", PERCENTAGE, CPU_ICON], "sensor_temp": ["sensors", "Temp", TEMP_CELSIUS, "mdi:thermometer"], "docker_active": ["docker", "Containers active", "", "mdi:docker"], - "docker_cpu_use": ["docker", "Containers CPU used", UNIT_PERCENTAGE, "mdi:docker"], + "docker_cpu_use": ["docker", "Containers CPU used", PERCENTAGE, "mdi:docker"], "docker_memory_use": [ "docker", "Containers RAM used", diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index e4ff1ab5b28..8b993b0e837 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -13,8 +13,8 @@ from homeassistant.const import ( CONF_STATE, CONF_TYPE, EVENT_HOMEASSISTANT_START, + PERCENTAGE, TIME_HOURS, - UNIT_PERCENTAGE, ) from homeassistant.core import CoreState, callback from homeassistant.exceptions import TemplateError @@ -41,7 +41,7 @@ CONF_TYPE_KEYS = [CONF_TYPE_TIME, CONF_TYPE_RATIO, CONF_TYPE_COUNT] DEFAULT_NAME = "unnamed statistics" UNITS = { CONF_TYPE_TIME: TIME_HOURS, - CONF_TYPE_RATIO: UNIT_PERCENTAGE, + CONF_TYPE_RATIO: PERCENTAGE, CONF_TYPE_COUNT: "", } ICON = "mdi:chart-line" diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index f768e28be92..e1ee75297fd 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -7,7 +7,7 @@ import homeconnect from homeconnect.api import HomeConnectError from homeassistant import config_entries, core -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, TIME_SECONDS, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE, TIME_SECONDS from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.dispatcher import dispatcher_send @@ -140,7 +140,7 @@ class DeviceWithPrograms(HomeConnectDevice): sensors = { "Remaining Program Time": (None, None, DEVICE_CLASS_TIMESTAMP, 1), "Duration": (TIME_SECONDS, "mdi:update", None, 1), - "Program Progress": (UNIT_PERCENTAGE, "mdi:progress-clock", None, 1), + "Program Progress": (PERCENTAGE, "mdi:progress-clock", None, 1), } return [ { diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 68b61772ce8..2fb61a61fed 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -24,11 +24,11 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, STATE_ON, STATE_UNAVAILABLE, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, __version__, ) from homeassistant.core import Context, callback as ha_callback, split_entity_id @@ -189,7 +189,7 @@ def get_accessory(hass, driver, state, aid, config): TEMP_FAHRENHEIT, ): a_type = "TemperatureSensor" - elif device_class == DEVICE_CLASS_HUMIDITY and unit == UNIT_PERCENTAGE: + elif device_class == DEVICE_CLASS_HUMIDITY and unit == PERCENTAGE: a_type = "HumiditySensor" elif device_class == DEVICE_CLASS_PM25 or DEVICE_CLASS_PM25 in state.entity_id: a_type = "AirQualitySensor" diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 7175198c4b5..dd829206b0c 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -17,10 +17,10 @@ from homeassistant.components.humidifier.const import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + PERCENTAGE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, - UNIT_PERCENTAGE, ) from homeassistant.core import callback from homeassistant.helpers.event import async_track_state_change_event @@ -215,7 +215,7 @@ class HumidifierDehumidifier(HomeAccessory): SERVICE_SET_HUMIDITY, {ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: humidity}, f"{self._target_humidity_char_name} to " - f"{char_values[self._target_humidity_char_name]}{UNIT_PERCENTAGE}", + f"{char_values[self._target_humidity_char_name]}{PERCENTAGE}", ) @callback diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 7f588af77fe..6d0f5f22d79 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -47,9 +47,9 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.core import callback @@ -376,7 +376,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug("%s: Set target humidity to %d", self.entity_id, value) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: value} self.call_service( - DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{UNIT_PERCENTAGE}" + DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{PERCENTAGE}" ) @callback diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index d1da2363843..944729d2e5c 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -7,8 +7,8 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback @@ -47,7 +47,7 @@ class HomeKitHumiditySensor(HomeKitEntity): @property def unit_of_measurement(self): """Return units for the sensor.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def state(self): @@ -195,7 +195,7 @@ class HomeKitBatterySensor(HomeKitEntity): @property def unit_of_measurement(self): """Return units for the sensor.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def is_low_battery(self): diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 6fa78602944..51a88bd1207 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -9,10 +9,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, + PERCENTAGE, POWER_WATT, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, - UNIT_PERCENTAGE, VOLT, VOLUME_CUBIC_METERS, ) @@ -38,7 +38,7 @@ HM_STATE_HA_CAST = { } HM_UNIT_HA_CAST = { - "HUMIDITY": UNIT_PERCENTAGE, + "HUMIDITY": PERCENTAGE, "TEMPERATURE": TEMP_CELSIUS, "ACTUAL_TEMPERATURE": TEMP_CELSIUS, "BRIGHTNESS": "#", @@ -62,7 +62,7 @@ HM_UNIT_HA_CAST = { "AIR_PRESSURE": "hPa", "FREQUENCY": FREQUENCY_HERTZ, "VALUE": "#", - "VALVE_STATE": UNIT_PERCENTAGE, + "VALVE_STATE": PERCENTAGE, } HM_DEVICE_CLASS_HA_CAST = { diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index c31e4d73cca..32191cde20e 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -30,10 +30,10 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, POWER_WATT, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.helpers.typing import HomeAssistantType @@ -156,7 +156,7 @@ class HomematicipAccesspointStatus(HomematicipGenericEntity): @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def device_state_attributes(self) -> Dict[str, Any]: @@ -195,7 +195,7 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity): @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return UNIT_PERCENTAGE + return PERCENTAGE class HomematicipHumiditySensor(HomematicipGenericEntity): @@ -218,7 +218,7 @@ class HomematicipHumiditySensor(HomematicipGenericEntity): @property def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" - return UNIT_PERCENTAGE + return PERCENTAGE class HomematicipTemperatureSensor(HomematicipGenericEntity): diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index 5bd77d4dcb2..5a0c81b083c 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -8,7 +8,7 @@ import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, TEMP_FAHRENHEIT, UNIT_PERCENTAGE +from homeassistant.const import CONF_NAME, PERCENTAGE, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -50,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= dev = [ HTU21DSensor(sensor_handler, name, SENSOR_TEMPERATURE, temp_unit), - HTU21DSensor(sensor_handler, name, SENSOR_HUMIDITY, UNIT_PERCENTAGE), + HTU21DSensor(sensor_handler, name, SENSOR_HUMIDITY, PERCENTAGE), ] async_add_entities(dev) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 0da8e77eeee..e96f844a5e1 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -10,8 +10,8 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.helpers.entity import Entity @@ -108,7 +108,7 @@ class HueBattery(GenericHueSensor): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return UNIT_PERCENTAGE + return PERCENTAGE SENSOR_CONFIG_MAP.update( diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index 6829c87708d..6bc9682f79a 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_FOR, CONF_PLATFORM, CONF_TYPE, - UNIT_PERCENTAGE, + PERCENTAGE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry @@ -114,10 +114,10 @@ async def async_get_trigger_capabilities(hass: HomeAssistant, config): "extra_fields": vol.Schema( { vol.Optional( - CONF_ABOVE, description={"suffix": UNIT_PERCENTAGE} + CONF_ABOVE, description={"suffix": PERCENTAGE} ): vol.Coerce(int), vol.Optional( - CONF_BELOW, description={"suffix": UNIT_PERCENTAGE} + CONF_BELOW, description={"suffix": PERCENTAGE} ): vol.Coerce(int), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 78c6fde75f5..589665b5a10 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -3,7 +3,7 @@ import logging from aiopvapi.resources.shade import factory as PvShade -from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.core import callback from .const import ( @@ -49,7 +49,7 @@ class PowerViewShadeBatterySensor(ShadeEntity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def name(self): diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index b2e8b4ead1e..37bbdf69703 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -3,7 +3,7 @@ import logging from typing import Dict from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -83,7 +83,7 @@ class IcloudDeviceBatterySensor(Entity): @property def unit_of_measurement(self) -> str: """Battery state measured in percentage.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def icon(self) -> str: diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index 1a655e242f9..89082a80830 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,11 +1,11 @@ """Support for Home Assistant iOS app sensors.""" from homeassistant.components import ios -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level SENSOR_TYPES = { - "level": ["Battery Level", UNIT_PERCENTAGE], + "level": ["Battery Level", PERCENTAGE], "state": ["Battery State", None], } diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 9f0c0d59346..6991e6d19ea 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta from typing import Any, Callable, Dict, List, Optional from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow @@ -114,7 +114,7 @@ class IPPMarkerSensor(IPPSensor): icon="mdi:water", key=f"marker_{marker_index}", name=f"{coordinator.data.info.name} {coordinator.data.markers[marker_index].name}", - unit_of_measurement=UNIT_PERCENTAGE, + unit_of_measurement=PERCENTAGE, ) @property diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index b2748223f51..d911fae2c82 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -56,6 +56,7 @@ from homeassistant.const import ( LENGTH_MILES, MASS_KILOGRAMS, MASS_POUNDS, + PERCENTAGE, POWER_WATT, PRESSURE_INHG, SERVICE_LOCK, @@ -83,7 +84,6 @@ from homeassistant.const import ( TIME_MONTHS, TIME_SECONDS, TIME_YEARS, - UNIT_PERCENTAGE, UV_INDEX, VOLT, VOLUME_GALLONS, @@ -359,7 +359,7 @@ UOM_FRIENDLY_NAME = { "48": SPEED_MILES_PER_HOUR, "49": SPEED_METERS_PER_SECOND, "50": "Ω", - "51": UNIT_PERCENTAGE, + "51": PERCENTAGE, "52": MASS_POUNDS, "53": "pf", "54": CONCENTRATION_PARTS_PER_MILLION, diff --git a/homeassistant/components/kaiterra/const.py b/homeassistant/components/kaiterra/const.py index 7f7eff99444..583cd60085d 100644 --- a/homeassistant/components/kaiterra/const.py +++ b/homeassistant/components/kaiterra/const.py @@ -7,7 +7,7 @@ from homeassistant.const import ( CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - UNIT_PERCENTAGE, + PERCENTAGE, ) DOMAIN = "kaiterra" @@ -54,7 +54,7 @@ ATTR_AQI_POLLUTANT = "air_quality_index_pollutant" AVAILABLE_AQI_STANDARDS = ["us", "cn", "in"] AVAILABLE_UNITS = [ "x", - UNIT_PERCENTAGE, + PERCENTAGE, "C", "F", CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 74554f2afc2..12c786c5aef 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -9,8 +9,8 @@ from homeassistant.const import ( CONF_ZONE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { DEVICE_CLASS_TEMPERATURE: ["Temperature", TEMP_CELSIUS], - DEVICE_CLASS_HUMIDITY: ["Humidity", UNIT_PERCENTAGE], + DEVICE_CLASS_HUMIDITY: ["Humidity", PERCENTAGE], } diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index f0ec885a8fe..2c7f5d294a9 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -14,8 +14,8 @@ from homeassistant.const import ( CONF_SENSORS, CONF_TYPE, EVENT_HOMEASSISTANT_STOP, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -194,7 +194,7 @@ class LaCrosseHumidity(LaCrosseSensor): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def state(self): diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index 219db45dbd4..821a7102154 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -3,10 +3,10 @@ from itertools import product from homeassistant.const import ( DEGREE, + PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN, - UNIT_PERCENTAGE, VOLT, ) @@ -160,7 +160,7 @@ VAR_UNITS = [ "LX", "M/S", "METERPERSECOND", - UNIT_PERCENTAGE, + PERCENTAGE, "PERCENT", "PPM", "VOLT", diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index f018a0c17c7..2144979106a 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -1,5 +1,5 @@ """Support for LightwaveRF TRV - Associated Battery.""" -from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.entity import Entity from . import CONF_SERIAL, LIGHTWAVE_LINK @@ -50,7 +50,7 @@ class LightwaveBattery(Entity): @property def unit_of_measurement(self): """Return the state of the sensor.""" - return UNIT_PERCENTAGE + return PERCENTAGE def update(self): """Communicate with a Lightwave RTF Proxy to get state.""" diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index bb1dd34dc00..f4d4e92cb78 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -6,12 +6,7 @@ from batinfo import Batteries import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_NAME, - CONF_NAME, - DEVICE_CLASS_BATTERY, - UNIT_PERCENTAGE, -) +from homeassistant.const import ATTR_NAME, CONF_NAME, DEVICE_CLASS_BATTERY, PERCENTAGE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -103,7 +98,7 @@ class LinuxBatterySensor(Entity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def device_state_attributes(self): diff --git a/homeassistant/components/logi_circle/const.py b/homeassistant/components/logi_circle/const.py index a0905aee63e..fb22338b2c7 100644 --- a/homeassistant/components/logi_circle/const.py +++ b/homeassistant/components/logi_circle/const.py @@ -1,5 +1,5 @@ """Constants in Logi Circle component.""" -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE DOMAIN = "logi_circle" DATA_LOGI = DOMAIN @@ -15,11 +15,11 @@ RECORDING_MODE_KEY = "RECORDING_MODE" # Sensor types: Name, unit of measure, icon per sensor key. LOGI_SENSORS = { - "battery_level": ["Battery", UNIT_PERCENTAGE, "battery-50"], + "battery_level": ["Battery", PERCENTAGE, "battery-50"], "last_activity_time": ["Last Activity", None, "history"], "recording": ["Recording Mode", None, "eye"], "signal_strength_category": ["WiFi Signal Category", None, "wifi"], - "signal_strength_percentage": ["WiFi Signal Strength", UNIT_PERCENTAGE, "wifi"], + "signal_strength_percentage": ["WiFi Signal Strength", PERCENTAGE, "wifi"], "streaming": ["Streaming Mode", None, "camera"], } diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index ed93d4a9791..9d184969139 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -12,8 +12,8 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SHOW_ON_MAP, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -43,7 +43,7 @@ TOPIC_UPDATE = f"{DOMAIN}_data_update" SENSORS = { SENSOR_TEMPERATURE: ["Temperature", "mdi:thermometer", TEMP_CELSIUS], - SENSOR_HUMIDITY: ["Humidity", "mdi:water-percent", UNIT_PERCENTAGE], + SENSOR_HUMIDITY: ["Humidity", "mdi:water-percent", PERCENTAGE], SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", "Pa"], SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:download", "Pa"], SENSOR_PM10: [ diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index fb960b3b26a..8b4d3a33501 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -4,10 +4,10 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) DOMAIN = "meteo_france" @@ -44,7 +44,7 @@ SENSOR_TYPES = { }, "rain_chance": { ENTITY_NAME: "Rain chance", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:weather-rainy", ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, @@ -52,7 +52,7 @@ SENSOR_TYPES = { }, "snow_chance": { ENTITY_NAME: "Snow chance", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:weather-snowy", ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, @@ -60,7 +60,7 @@ SENSOR_TYPES = { }, "freeze_chance": { ENTITY_NAME: "Freeze chance", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:snowflake", ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, @@ -116,7 +116,7 @@ SENSOR_TYPES = { }, "cloud": { ENTITY_NAME: "Cloud cover", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:weather-partly-cloudy", ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index e314423a0a5..f8fabd56dce 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -7,9 +7,9 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, LENGTH_KILOMETERS, + PERCENTAGE, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, - UNIT_PERCENTAGE, UV_INDEX, ) from homeassistant.core import callback @@ -74,11 +74,11 @@ SENSOR_TYPES = { "precipitation": [ "Probability of Precipitation", None, - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:weather-rainy", True, ], - "humidity": ["Humidity", DEVICE_CLASS_HUMIDITY, UNIT_PERCENTAGE, None, False], + "humidity": ["Humidity", DEVICE_CLASS_HUMIDITY, PERCENTAGE, None, False], } diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index c857cc731f4..c349589ebdd 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -6,7 +6,7 @@ from pycsspeechtts import pycsspeechtts import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider -from homeassistant.const import CONF_API_KEY, CONF_TYPE, UNIT_PERCENTAGE +from homeassistant.const import CONF_API_KEY, CONF_TYPE, PERCENTAGE import homeassistant.helpers.config_validation as cv CONF_GENDER = "gender" @@ -122,8 +122,8 @@ class MicrosoftProvider(Provider): self._gender = gender self._type = ttype self._output = DEFAULT_OUTPUT - self._rate = f"{rate}{UNIT_PERCENTAGE}" - self._volume = f"{volume}{UNIT_PERCENTAGE}" + self._rate = f"{rate}{PERCENTAGE}" + self._volume = f"{volume}{PERCENTAGE}" self._pitch = pitch self._contour = contour self._region = region diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 3b5d7166425..6206c67dc03 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -17,9 +17,9 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, + PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -54,9 +54,9 @@ ATTR_LAST_SUCCESSFUL_UPDATE = "last_successful_update" SENSOR_TYPES = { "temperature": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"], "light": ["Light intensity", "lx", "mdi:white-balance-sunny"], - "moisture": ["Moisture", UNIT_PERCENTAGE, "mdi:water-percent"], + "moisture": ["Moisture", PERCENTAGE, "mdi:water-percent"], "conductivity": ["Conductivity", CONDUCTIVITY, "mdi:flash-circle"], - "battery": ["Battery", UNIT_PERCENTAGE, "mdi:battery-charging"], + "battery": ["Battery", PERCENTAGE, "mdi:battery-charging"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index 745ede3b9e8..6b64c88c1ce 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -15,8 +15,8 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -48,8 +48,8 @@ DEFAULT_TIMEOUT = 10 # Sensor types are defined like: Name, units SENSOR_TYPES = { "temperature": [DEVICE_CLASS_TEMPERATURE, "Temperature", TEMP_CELSIUS], - "humidity": [DEVICE_CLASS_HUMIDITY, "Humidity", UNIT_PERCENTAGE], - "battery": [DEVICE_CLASS_BATTERY, "Battery", UNIT_PERCENTAGE], + "humidity": [DEVICE_CLASS_HUMIDITY, "Humidity", PERCENTAGE], + "battery": [DEVICE_CLASS_BATTERY, "Battery", PERCENTAGE], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index c546a8d3337..81ae571ed79 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -10,10 +10,10 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, EVENT_HOMEASSISTANT_START, + PERCENTAGE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -247,7 +247,7 @@ class MoldIndicator(Entity): ) return None - if unit != UNIT_PERCENTAGE: + if unit != PERCENTAGE: _LOGGER.error( "Humidity sensor %s has unsupported unit: %s %s", state.entity_id, @@ -362,7 +362,7 @@ class MoldIndicator(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def state(self): diff --git a/homeassistant/components/mychevy/sensor.py b/homeassistant/components/mychevy/sensor.py index 96e0eef68ad..41311845185 100644 --- a/homeassistant/components/mychevy/sensor.py +++ b/homeassistant/components/mychevy/sensor.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -27,7 +27,7 @@ SENSORS = [ EVSensorConfig("Charged By", "estimatedFullChargeBy"), EVSensorConfig("Charge Mode", "chargeMode"), EVSensorConfig( - "Battery Level", BATTERY_SENSOR, UNIT_PERCENTAGE, "mdi:battery", ["charging"] + "Battery Level", BATTERY_SENSOR, PERCENTAGE, "mdi:battery", ["charging"] ), ] diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index d170f476158..8ff2139a7b4 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -10,18 +10,18 @@ from homeassistant.const import ( FREQUENCY_HERTZ, LENGTH_METERS, MASS_KILOGRAMS, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, VOLT, ) SENSORS = { "V_TEMP": [None, "mdi:thermometer"], - "V_HUM": [UNIT_PERCENTAGE, "mdi:water-percent"], - "V_DIMMER": [UNIT_PERCENTAGE, "mdi:percent"], - "V_PERCENTAGE": [UNIT_PERCENTAGE, "mdi:percent"], + "V_HUM": [PERCENTAGE, "mdi:water-percent"], + "V_DIMMER": [PERCENTAGE, "mdi:percent"], + "V_PERCENTAGE": [PERCENTAGE, "mdi:percent"], "V_PRESSURE": [None, "mdi:gauge"], "V_FORECAST": [None, "mdi:weather-partly-cloudy"], "V_RAIN": [None, "mdi:weather-rainy"], @@ -34,7 +34,7 @@ SENSORS = { "V_IMPEDANCE": ["ohm", None], "V_WATT": [POWER_WATT, None], "V_KWH": [ENERGY_KILO_WATT_HOUR, None], - "V_LIGHT_LEVEL": [UNIT_PERCENTAGE, "mdi:white-balance-sunny"], + "V_LIGHT_LEVEL": [PERCENTAGE, "mdi:white-balance-sunny"], "V_FLOW": [LENGTH_METERS, "mdi:gauge"], "V_VOLUME": ["m³", None], "V_LEVEL": { diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 17973df06bd..efcbfb8d54c 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -5,7 +5,7 @@ import logging from pybotvac.exceptions import NeatoRobotException from homeassistant.components.sensor import DEVICE_CLASS_BATTERY -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.helpers.entity import Entity from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES @@ -86,7 +86,7 @@ class NeatoSensor(Entity): @property def unit_of_measurement(self): """Return unit of measurement.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def device_info(self): diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index f7e727e0e2d..6844269400a 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -5,10 +5,10 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from . import CONF_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice @@ -42,7 +42,7 @@ _VALID_SENSOR_TYPES = ( + STRUCTURE_CAMERA_SENSOR_TYPES ) -SENSOR_UNITS = {"humidity": UNIT_PERCENTAGE} +SENSOR_UNITS = {"humidity": PERCENTAGE} SENSOR_DEVICE_CLASSES = {"humidity": DEVICE_CLASS_HUMIDITY} diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 4dadd101e39..2368d54efdf 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -12,10 +12,10 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, PRESSURE_MBAR, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import async_entries_for_config_entry @@ -51,13 +51,13 @@ SENSOR_TYPES = { "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2", None], "pressure": ["Pressure", PRESSURE_MBAR, None, DEVICE_CLASS_PRESSURE], "noise": ["Noise", "dB", "mdi:volume-high", None], - "humidity": ["Humidity", UNIT_PERCENTAGE, None, DEVICE_CLASS_HUMIDITY], + "humidity": ["Humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY], "rain": ["Rain", "mm", "mdi:weather-rainy", None], "sum_rain_1": ["Rain last hour", "mm", "mdi:weather-rainy", None], "sum_rain_24": ["Rain last 24h", "mm", "mdi:weather-rainy", None], "battery_vp": ["Battery", "", "mdi:battery", None], "battery_lvl": ["Battery Level", "", "mdi:battery", None], - "battery_percent": ["Battery Percent", UNIT_PERCENTAGE, None, DEVICE_CLASS_BATTERY], + "battery_percent": ["Battery Percent", PERCENTAGE, None, DEVICE_CLASS_BATTERY], "min_temp": ["Min Temp.", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], "max_temp": ["Max Temp.", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], "windangle": ["Direction", None, "mdi:compass-outline", None], diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 69bbc200aa2..64c8d789fc7 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_RESOURCES, - UNIT_PERCENTAGE, + PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -79,7 +79,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: resource_data = netdata.api.metrics[sensor] unit = ( - UNIT_PERCENTAGE + PERCENTAGE if resource_data["units"] == "percentage" else resource_data["units"] ) diff --git a/homeassistant/components/netgear_lte/sensor_types.py b/homeassistant/components/netgear_lte/sensor_types.py index 883b4803544..e354c84e715 100644 --- a/homeassistant/components/netgear_lte/sensor_types.py +++ b/homeassistant/components/netgear_lte/sensor_types.py @@ -1,7 +1,7 @@ """Define possible sensor types.""" from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY -from homeassistant.const import DATA_MEBIBYTES, UNIT_PERCENTAGE +from homeassistant.const import DATA_MEBIBYTES, PERCENTAGE SENSOR_SMS = "sms" SENSOR_SMS_TOTAL = "sms_total" @@ -11,7 +11,7 @@ SENSOR_UNITS = { SENSOR_SMS: "unread", SENSOR_SMS_TOTAL: "messages", SENSOR_USAGE: DATA_MEBIBYTES, - "radio_quality": UNIT_PERCENTAGE, + "radio_quality": PERCENTAGE, "rx_level": "dBm", "tx_level": "dBm", "upstream": None, diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index ea2fd2b5718..eff15d443bc 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -5,9 +5,9 @@ from nexia.const import UNIT_CELSIUS from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR @@ -57,7 +57,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "get_current_compressor_speed", "Current Compressor Speed", None, - UNIT_PERCENTAGE, + PERCENTAGE, percent_conv, ) ) @@ -68,7 +68,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "get_requested_compressor_speed", "Requested Compressor Speed", None, - UNIT_PERCENTAGE, + PERCENTAGE, percent_conv, ) ) @@ -98,7 +98,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "get_relative_humidity", "Relative Humidity", DEVICE_CLASS_HUMIDITY, - UNIT_PERCENTAGE, + PERCENTAGE, percent_conv, ) ) diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index db2b22650ef..369a5c875ac 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -14,7 +14,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_HOST, CONF_TIMEOUT, HTTP_OK, UNIT_PERCENTAGE +from homeassistant.const import CONF_HOST, CONF_TIMEOUT, HTTP_OK, PERCENTAGE import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -68,11 +68,11 @@ POSITIONS = { TRANSPARENCIES = { "default": 0, - f"0{UNIT_PERCENTAGE}": 1, - f"25{UNIT_PERCENTAGE}": 2, - f"50{UNIT_PERCENTAGE}": 3, - f"75{UNIT_PERCENTAGE}": 4, - f"100{UNIT_PERCENTAGE}": 5, + f"0{PERCENTAGE}": 1, + f"25{PERCENTAGE}": 2, + f"50{PERCENTAGE}": 3, + f"75{PERCENTAGE}": 4, + f"100{PERCENTAGE}": 5, } COLORS = { diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 3565fb303d5..368db17ab4b 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -1,7 +1,7 @@ """Battery Charge and Range Support for the Nissan Leaf.""" import logging -from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM @@ -56,7 +56,7 @@ class LeafBatterySensor(LeafEntity): @property def unit_of_measurement(self): """Battery state measured in percentage.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def icon(self): diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 020ee7aea4a..cc70b33f763 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -8,10 +8,10 @@ from homeassistant.const import ( ELECTRICAL_CURRENT_AMPERE, ELECTRICAL_VOLT_AMPERE, FREQUENCY_HERTZ, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, TIME_SECONDS, - UNIT_PERCENTAGE, VOLT, ) @@ -48,8 +48,8 @@ SENSOR_TYPES = { "mdi:thermometer", DEVICE_CLASS_TEMPERATURE, ], - "ups.load": ["Load", UNIT_PERCENTAGE, "mdi:gauge", None], - "ups.load.high": ["Overload Setting", UNIT_PERCENTAGE, "mdi:gauge", None], + "ups.load": ["Load", PERCENTAGE, "mdi:gauge", None], + "ups.load.high": ["Overload Setting", PERCENTAGE, "mdi:gauge", None], "ups.id": ["System identifier", "", "mdi:information-outline", None], "ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer-outline", None], "ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer-outline", None], @@ -77,7 +77,7 @@ SENSOR_TYPES = { "ups.test.date": ["Self-Test Date", "", "mdi:calendar", None], "ups.display.language": ["Language", "", "mdi:information-outline", None], "ups.contacts": ["External Contacts", "", "mdi:information-outline", None], - "ups.efficiency": ["Efficiency", UNIT_PERCENTAGE, "mdi:gauge", None], + "ups.efficiency": ["Efficiency", PERCENTAGE, "mdi:gauge", None], "ups.power": ["Current Apparent Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash", None], "ups.power.nominal": ["Nominal Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash", None], "ups.realpower": [ @@ -101,20 +101,20 @@ SENSOR_TYPES = { "ups.shutdown": ["Shutdown Ability", "", "mdi:information-outline", None], "battery.charge": [ "Battery Charge", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:gauge", DEVICE_CLASS_BATTERY, ], - "battery.charge.low": ["Low Battery Setpoint", UNIT_PERCENTAGE, "mdi:gauge", None], + "battery.charge.low": ["Low Battery Setpoint", PERCENTAGE, "mdi:gauge", None], "battery.charge.restart": [ "Minimum Battery to Start", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:gauge", None, ], "battery.charge.warning": [ "Warning Battery Setpoint", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:gauge", None, ], diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index c345dd6cce7..373988ca08a 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -18,9 +18,9 @@ from homeassistant.const import ( CONF_SENSORS, CONF_SSL, CONTENT_TYPE_JSON, + PERCENTAGE, TEMP_CELSIUS, TIME_SECONDS, - UNIT_PERCENTAGE, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -76,7 +76,7 @@ SENSOR_TYPES = { "job", "progress", "completion", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:file-percent", ], "Time Remaining": [ diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index c7b81df2ef8..921f355edbe 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -3,7 +3,7 @@ import logging import requests -from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from . import DOMAIN as COMPONENT_DOMAIN, SENSOR_TYPES @@ -111,7 +111,7 @@ class OctoPrintSensor(Entity): def state(self): """Return the state of the sensor.""" sensor_unit = self.unit_of_measurement - if sensor_unit in (TEMP_CELSIUS, UNIT_PERCENTAGE): + if sensor_unit in (TEMP_CELSIUS, PERCENTAGE): # API sometimes returns null and not 0 if self._state is None: self._state = 0 diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index f12d3ab2346..148c596e130 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -12,8 +12,8 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, ELECTRICAL_CURRENT_AMPERE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, VOLT, ) import homeassistant.helpers.config_validation as cv @@ -67,14 +67,14 @@ HOBBYBOARD_EF = { SENSOR_TYPES = { # SensorType: [ Measured unit, Unit ] "temperature": ["temperature", TEMP_CELSIUS], - "humidity": ["humidity", UNIT_PERCENTAGE], - "humidity_raw": ["humidity", UNIT_PERCENTAGE], + "humidity": ["humidity", PERCENTAGE], + "humidity_raw": ["humidity", PERCENTAGE], "pressure": ["pressure", "mb"], "illuminance": ["illuminance", "lux"], - "wetness_0": ["wetness", UNIT_PERCENTAGE], - "wetness_1": ["wetness", UNIT_PERCENTAGE], - "wetness_2": ["wetness", UNIT_PERCENTAGE], - "wetness_3": ["wetness", UNIT_PERCENTAGE], + "wetness_0": ["wetness", PERCENTAGE], + "wetness_1": ["wetness", PERCENTAGE], + "wetness_2": ["wetness", PERCENTAGE], + "wetness_3": ["wetness", PERCENTAGE], "moisture_0": ["moisture", "cb"], "moisture_1": ["moisture", "cb"], "moisture_2": ["moisture", "cb"], diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 5be29522535..0b696ed9339 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -3,10 +3,10 @@ import pyotgw.vars as gw_vars from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, TIME_HOURS, TIME_MINUTES, - UNIT_PERCENTAGE, ) ATTR_GW_ID = "gateway_id" @@ -124,7 +124,7 @@ SENSOR_INFO = { gw_vars.DATA_MASTER_MEMBERID: [None, None, "Thermostat Member ID {}"], gw_vars.DATA_SLAVE_MEMBERID: [None, None, "Boiler Member ID {}"], gw_vars.DATA_SLAVE_OEM_FAULT: [None, None, "Boiler OEM Fault Code {}"], - gw_vars.DATA_COOLING_CONTROL: [None, UNIT_PERCENTAGE, "Cooling Control Signal {}"], + gw_vars.DATA_COOLING_CONTROL: [None, PERCENTAGE, "Cooling Control Signal {}"], gw_vars.DATA_CONTROL_SETPOINT_2: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, @@ -137,13 +137,13 @@ SENSOR_INFO = { ], gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD: [ None, - UNIT_PERCENTAGE, + PERCENTAGE, "Boiler Maximum Relative Modulation {}", ], gw_vars.DATA_SLAVE_MAX_CAPACITY: [None, UNIT_KW, "Boiler Maximum Capacity {}"], gw_vars.DATA_SLAVE_MIN_MOD_LEVEL: [ None, - UNIT_PERCENTAGE, + PERCENTAGE, "Boiler Minimum Modulation Level {}", ], gw_vars.DATA_ROOM_SETPOINT: [ @@ -151,7 +151,7 @@ SENSOR_INFO = { TEMP_CELSIUS, "Room Setpoint {}", ], - gw_vars.DATA_REL_MOD_LEVEL: [None, UNIT_PERCENTAGE, "Relative Modulation Level {}"], + gw_vars.DATA_REL_MOD_LEVEL: [None, PERCENTAGE, "Relative Modulation Level {}"], gw_vars.DATA_CH_WATER_PRESS: [None, UNIT_BAR, "Central Heating Water Pressure {}"], gw_vars.DATA_DHW_FLOW_RATE: [None, UNIT_L_MIN, "Hot Water Flow Rate {}"], gw_vars.DATA_ROOM_SETPOINT_2: [ diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index f2507527499..bc7a428f366 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -14,10 +14,10 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, PRESSURE_PA, SPEED_METERS_PER_SECOND, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) DOMAIN = "openweathermap" @@ -103,7 +103,7 @@ WEATHER_SENSOR_TYPES = { ATTR_API_WIND_BEARING: {SENSOR_NAME: "Wind bearing", SENSOR_UNIT: DEGREE}, ATTR_API_HUMIDITY: { SENSOR_NAME: "Humidity", - SENSOR_UNIT: UNIT_PERCENTAGE, + SENSOR_UNIT: PERCENTAGE, SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, }, ATTR_API_PRESSURE: { @@ -111,7 +111,7 @@ WEATHER_SENSOR_TYPES = { SENSOR_UNIT: PRESSURE_PA, SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, }, - ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: UNIT_PERCENTAGE}, + ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: PERCENTAGE}, ATTR_API_RAIN: {SENSOR_NAME: "Rain", SENSOR_UNIT: "mm"}, ATTR_API_SNOW: {SENSOR_NAME: "Snow", SENSOR_UNIT: "mm"}, ATTR_API_CONDITION: {SENSOR_NAME: "Condition"}, diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index cb8087fdbf0..b15db5f3980 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,7 +1,7 @@ """Constants for the pi_hole integration.""" from datetime import timedelta -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE DOMAIN = "pi_hole" @@ -27,7 +27,7 @@ SENSOR_DICT = { "ads_blocked_today": ["Ads Blocked Today", "ads", "mdi:close-octagon-outline"], "ads_percentage_today": [ "Ads Percentage Blocked Today", - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:close-octagon-outline", ], "clients_ever_seen": ["Seen Clients", "clients", "mdi:account-outline"], diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index aee4358acdf..3f8034698fd 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -2,7 +2,7 @@ import logging -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -146,7 +146,7 @@ class PlaatoSensor(Entity): if self._type == ATTR_BPM: return "bpm" if self._type == ATTR_ABV: - return UNIT_PERCENTAGE + return PERCENTAGE return "" diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 81e27928c6b..d78b12c06e0 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -12,12 +12,12 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONDUCTIVITY, CONF_SENSORS, + PERCENTAGE, STATE_OK, STATE_PROBLEM, STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -134,7 +134,7 @@ class Plant(Entity): READINGS = { READING_BATTERY: { - ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, "min": CONF_MIN_BATTERY_LEVEL, }, READING_TEMPERATURE: { @@ -143,7 +143,7 @@ class Plant(Entity): "max": CONF_MAX_TEMPERATURE, }, READING_MOISTURE: { - ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, "min": CONF_MIN_MOISTURE, "max": CONF_MAX_MOISTURE, }, diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 39c6b6e5010..1e1cb607ff4 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -10,10 +10,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + PERCENTAGE, POWER_WATT, PRESSURE_BAR, TEMP_CELSIUS, - UNIT_PERCENTAGE, VOLUME_CUBIC_METERS, ) from homeassistant.core import callback @@ -41,7 +41,7 @@ ATTR_TEMPERATURE = [ ] ATTR_BATTERY_LEVEL = [ "Charge", - UNIT_PERCENTAGE, + PERCENTAGE, DEVICE_CLASS_BATTERY, ] ATTR_ILLUMINANCE = [ @@ -147,8 +147,8 @@ ENERGY_SENSOR_MAP = { MISC_SENSOR_MAP = { "battery": ATTR_BATTERY_LEVEL, "illuminance": ATTR_ILLUMINANCE, - "modulation_level": ["Heater Modulation Level", UNIT_PERCENTAGE, None], - "valve_position": ["Valve Position", UNIT_PERCENTAGE, None], + "modulation_level": ["Heater Modulation Level", PERCENTAGE, None], + "valve_position": ["Valve Position", PERCENTAGE, None], "water_pressure": ATTR_PRESSURE, } @@ -297,7 +297,7 @@ class PwThermostatSensor(SmileSensor, Entity): measurement = data[self._sensor] if self._sensor == "battery" or self._sensor == "valve_position": measurement = measurement * 100 - if self._unit_of_measurement == UNIT_PERCENTAGE: + if self._unit_of_measurement == PERCENTAGE: measurement = int(measurement) self._state = measurement self._icon = CUSTOM_ICONS.get(self._sensor, self._icon) diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index 70fe1ef0b6d..4ac8f0c1832 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -6,8 +6,8 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import parse_datetime @@ -22,7 +22,7 @@ DEVICE_CLASS_SOUND = "sound_level" SENSOR_TYPES = { DEVICE_CLASS_TEMPERATURE: (None, 1, TEMP_CELSIUS), DEVICE_CLASS_PRESSURE: (None, 0, "hPa"), - DEVICE_CLASS_HUMIDITY: (None, 1, UNIT_PERCENTAGE), + DEVICE_CLASS_HUMIDITY: (None, 1, PERCENTAGE), DEVICE_CLASS_SOUND: ("mdi:ear-hearing", 1, "dBa"), } diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py index 098db73c258..b79f7499bcd 100644 --- a/homeassistant/components/poolsense/sensor.py +++ b/homeassistant/components/poolsense/sensor.py @@ -7,8 +7,8 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.helpers.entity import Entity @@ -26,7 +26,7 @@ SENSORS = { }, "pH": {"unit": None, "icon": "mdi:pool", "name": "pH", "device_class": None}, "Battery": { - "unit": UNIT_PERCENTAGE, + "unit": PERCENTAGE, "icon": None, "name": "Battery", "device_class": DEVICE_CLASS_BATTERY, diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index ade7746df30..ff8830c3970 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -3,11 +3,7 @@ import logging from tesla_powerwall import MeterType, convert_to_kw -from homeassistant.const import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_POWER, - UNIT_PERCENTAGE, -) +from homeassistant.const import DEVICE_CLASS_BATTERY, DEVICE_CLASS_POWER, PERCENTAGE from .const import ( ATTR_ENERGY_EXPORTED, @@ -69,7 +65,7 @@ class PowerWallChargeSensor(PowerWallEntity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def name(self): diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index d9aa4bd6090..87a8a2c41f3 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -24,11 +24,11 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_TEXT_PLAIN, EVENT_STATE_CHANGED, + PERCENTAGE, STATE_ON, STATE_UNAVAILABLE, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.helpers import entityfilter, state as state_helper import homeassistant.helpers.config_validation as cv @@ -439,7 +439,7 @@ class PrometheusMetrics: units = { TEMP_CELSIUS: "c", TEMP_FAHRENHEIT: "c", # F should go into C metric - UNIT_PERCENTAGE: "percent", + PERCENTAGE: "percent", } default = unit.replace("/", "_per_") default = default.lower() diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index aefbe760d6a..fe4003a9423 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -18,8 +18,8 @@ from homeassistant.const import ( CONF_VERIFY_SSL, DATA_GIBIBYTES, DATA_RATE_MEBIBYTES_PER_SECOND, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -62,12 +62,12 @@ _SYSTEM_MON_COND = { } _CPU_MON_COND = { "cpu_temp": ["CPU Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "cpu_usage": ["CPU Usage", UNIT_PERCENTAGE, "mdi:chip"], + "cpu_usage": ["CPU Usage", PERCENTAGE, "mdi:chip"], } _MEMORY_MON_COND = { "memory_free": ["Memory Available", DATA_GIBIBYTES, "mdi:memory"], "memory_used": ["Memory Used", DATA_GIBIBYTES, "mdi:memory"], - "memory_percent_used": ["Memory Usage", UNIT_PERCENTAGE, "mdi:memory"], + "memory_percent_used": ["Memory Usage", PERCENTAGE, "mdi:memory"], } _NETWORK_MON_COND = { "network_link_status": ["Network Link", None, "mdi:checkbox-marked-circle-outline"], @@ -81,7 +81,7 @@ _DRIVE_MON_COND = { _VOLUME_MON_COND = { "volume_size_used": ["Used Space", DATA_GIBIBYTES, "mdi:chart-pie"], "volume_size_free": ["Free Space", DATA_GIBIBYTES, "mdi:chart-pie"], - "volume_percentage_used": ["Volume Used", UNIT_PERCENTAGE, "mdi:chart-pie"], + "volume_percentage_used": ["Volume Used", PERCENTAGE, "mdi:chart-pie"], } _MONITORED_CONDITIONS = ( diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index 17faea4495f..5955ef67168 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -11,9 +11,9 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, + PERCENTAGE, TIME_DAYS, TIME_MINUTES, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send @@ -59,7 +59,7 @@ ICON_MAP = { UNIT_OF_MEASUREMENT_MAP = { "auto_watering": "", - "battery": UNIT_PERCENTAGE, + "battery": PERCENTAGE, "is_watering": "", "manual_watering": "", "next_cycle": "", diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index a5a9224464d..a680fd77761 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -12,8 +12,8 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_SENSORS, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -124,7 +124,7 @@ SENSOR_TYPES = { "_chamber_", ], "current_state": ["state", None, "mdi:printer-3d", ""], - "current_job": ["progress", UNIT_PERCENTAGE, "mdi:file-percent", "_current_job"], + "current_job": ["progress", PERCENTAGE, "mdi:file-percent", "_current_job"], "job_end": ["progress", None, "mdi:clock-end", "_job_end"], "job_start": ["progress", None, "mdi:clock-start", "_job_start"], } diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index a7f7425fb81..6082d54df12 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -19,9 +19,9 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, - UNIT_PERCENTAGE, UV_INDEX, ) from homeassistant.core import callback @@ -51,7 +51,7 @@ DATA_TYPES = OrderedDict( [ ("Temperature", TEMP_CELSIUS), ("Temperature2", TEMP_CELSIUS), - ("Humidity", UNIT_PERCENTAGE), + ("Humidity", PERCENTAGE), ("Barometer", ""), ("Wind direction", ""), ("Rain rate", ""), @@ -76,7 +76,7 @@ DATA_TYPES = OrderedDict( ("Energy usage", ""), ("Voltage", ""), ("Current", ""), - ("Battery numeric", UNIT_PERCENTAGE), + ("Battery numeric", PERCENTAGE), ("Rssi numeric", "dBm"), ] ) diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 260b4170745..24a5cd3b6fb 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,7 +1,7 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" import logging -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -202,7 +202,7 @@ SENSOR_TYPES = { "battery": [ "Battery", ["doorbots", "authorized_doorbots", "stickup_cams"], - UNIT_PERCENTAGE, + PERCENTAGE, None, None, "battery", diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index 435f26510f8..54ea1eade0d 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.vacuum import STATE_DOCKED -from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.icon import icon_for_battery_level from .const import BLID, DOMAIN, ROOMBA_SESSION @@ -41,7 +41,7 @@ class RoombaBattery(IRobotEntity): @property def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def icon(self): diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 29aa4af967e..3966e52f1a8 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -10,8 +10,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_DISPLAY_OPTIONS, CONF_NAME, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -26,7 +26,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SENSOR_TYPES = { "temperature": ["temperature", TEMP_CELSIUS], - "humidity": ["humidity", UNIT_PERCENTAGE], + "humidity": ["humidity", PERCENTAGE], "pressure": ["pressure", "mb"], } diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index f10e0864061..618abc994b1 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -7,10 +7,10 @@ from homeassistant.const import ( DEGREE, ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, VOLT, ) from homeassistant.helpers.entity import Entity @@ -19,18 +19,18 @@ from . import ShellyBlockEntity, ShellyDeviceWrapper from .const import DOMAIN SENSORS = { - "battery": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_BATTERY], + "battery": [PERCENTAGE, sensor.DEVICE_CLASS_BATTERY], "concentration": [CONCENTRATION_PARTS_PER_MILLION, None], "current": [ELECTRICAL_CURRENT_AMPERE, sensor.DEVICE_CLASS_CURRENT], "deviceTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], "energy": [ENERGY_KILO_WATT_HOUR, sensor.DEVICE_CLASS_ENERGY], "energyReturned": [ENERGY_KILO_WATT_HOUR, sensor.DEVICE_CLASS_ENERGY], "extTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], - "humidity": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_HUMIDITY], + "humidity": [PERCENTAGE, sensor.DEVICE_CLASS_HUMIDITY], "luminosity": ["lx", sensor.DEVICE_CLASS_ILLUMINANCE], "overpowerValue": [POWER_WATT, sensor.DEVICE_CLASS_POWER], "power": [POWER_WATT, sensor.DEVICE_CLASS_POWER], - "powerFactor": [UNIT_PERCENTAGE, sensor.DEVICE_CLASS_POWER_FACTOR], + "powerFactor": [PERCENTAGE, sensor.DEVICE_CLASS_POWER_FACTOR], "tilt": [DEGREE, None], "voltage": [VOLT, sensor.DEVICE_CLASS_VOLTAGE], } diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index 3a66b47688c..277039b3ba6 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -11,9 +11,9 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, + PERCENTAGE, PRECISION_TENTHS, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -141,7 +141,7 @@ class SHTSensorHumidity(SHTSensor): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return UNIT_PERCENTAGE + return PERCENTAGE def update(self): """Fetch humidity from the sensor.""" diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index d976e6e9408..9b759327dca 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -13,9 +13,9 @@ from homeassistant.const import ( CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP, + PERCENTAGE, STATE_UNKNOWN, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -83,7 +83,7 @@ class SkybeaconHumid(Entity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def device_state_attributes(self): diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 8a6cf3cc215..7de3b98b1da 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -13,10 +13,10 @@ from homeassistant.const import ( DEVICE_CLASS_TIMESTAMP, ENERGY_KILO_WATT_HOUR, MASS_KILOGRAMS, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, VOLT, ) @@ -36,9 +36,9 @@ CAPABILITY_TO_SENSORS = { Map(Attribute.air_quality, "Air Quality", "CAQI", None) ], Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None)], - Capability.audio_volume: [Map(Attribute.volume, "Volume", UNIT_PERCENTAGE, None)], + Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None)], Capability.battery: [ - Map(Attribute.battery, "Battery", UNIT_PERCENTAGE, DEVICE_CLASS_BATTERY) + Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY) ], Capability.body_mass_index_measurement: [ Map(Attribute.bmi_measurement, "Body Mass Index", f"{MASS_KILOGRAMS}/m^2", None) @@ -113,7 +113,7 @@ CAPABILITY_TO_SENSORS = { Map(Attribute.illuminance, "Illuminance", "lux", DEVICE_CLASS_ILLUMINANCE) ], Capability.infrared_level: [ - Map(Attribute.infrared_level, "Infrared Level", UNIT_PERCENTAGE, None) + Map(Attribute.infrared_level, "Infrared Level", PERCENTAGE, None) ], Capability.media_input_source: [ Map(Attribute.input_source, "Media Input Source", None, None) @@ -151,7 +151,7 @@ CAPABILITY_TO_SENSORS = { Map( Attribute.humidity, "Relative Humidity Measurement", - UNIT_PERCENTAGE, + PERCENTAGE, DEVICE_CLASS_HUMIDITY, ) ], diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 0749cb54827..6b4c0bc233f 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -1,7 +1,7 @@ """Constants for the SolarEdge Monitoring API.""" from datetime import timedelta -from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT, UNIT_PERCENTAGE +from homeassistant.const import ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT DOMAIN = "solaredge" @@ -77,5 +77,5 @@ SENSOR_TYPES = { False, ], "feedin_power": ["FeedIn", "Exported Power", None, "mdi:flash", False], - "storage_level": ["STORAGE", "Storage Level", UNIT_PERCENTAGE, None, False], + "storage_level": ["STORAGE", "Storage Level", PERCENTAGE, None, False], } diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 07dd101b943..dab844f86d6 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -1,7 +1,7 @@ """Constants for the Solar-Log integration.""" from datetime import timedelta -from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT, UNIT_PERCENTAGE, VOLT +from homeassistant.const import ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, VOLT DOMAIN = "solarlog" @@ -77,7 +77,7 @@ SENSOR_TYPES = { POWER_WATT, "mdi:solar-power", ], - "capacity": ["CAPACITY", "capacity", UNIT_PERCENTAGE, "mdi:solar-power"], + "capacity": ["CAPACITY", "capacity", PERCENTAGE, "mdi:solar-power"], "efficiency": [ "EFFICIENCY", "efficiency", diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 83392b31380..2de4647aa94 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,6 +1,6 @@ """Reads vehicle status from StarLine API.""" from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE -from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE, VOLT +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, VOLT from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level @@ -13,7 +13,7 @@ SENSOR_TYPES = { "balance": ["Balance", None, None, "mdi:cash-multiple"], "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], - "gsm_lvl": ["GSM Signal", None, UNIT_PERCENTAGE, None], + "gsm_lvl": ["GSM Signal", None, PERCENTAGE, None], } diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 19c80a82e9f..f6867d83212 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_NAME, DATA_GIGABYTES, HTTP_OK, - UNIT_PERCENTAGE, + PERCENTAGE, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -30,7 +30,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) REQUEST_TIMEOUT = 5 # seconds SENSOR_TYPES = { - "usage": ["Usage Ratio", UNIT_PERCENTAGE, "mdi:percent"], + "usage": ["Usage Ratio", PERCENTAGE, "mdi:percent"], "usage_gb": ["Usage", DATA_GIGABYTES, "mdi:download"], "limit": ["Data limit", DATA_GIGABYTES, "mdi:download"], "used_download": ["Used Download", DATA_GIGABYTES, "mdi:download"], diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index 9c1416479cb..d3fcb41dbf4 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import ( CONF_ID, CONF_TYPE, DEVICE_CLASS_BATTERY, - UNIT_PERCENTAGE, + PERCENTAGE, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -181,4 +181,4 @@ class SureBattery(SurePetcareSensor): @property def unit_of_measurement(self) -> str: """Return the unit of measurement.""" - return UNIT_PERCENTAGE + return PERCENTAGE diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 869a2a8e997..639ec3ac6cb 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_NAME, CONF_RESOURCE, CONF_URL, UNIT_PERCENTAGE +from homeassistant.const import CONF_NAME, CONF_RESOURCE, CONF_URL, PERCENTAGE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -166,7 +166,7 @@ class SyncThruTonerSensor(SyncThruSensor): super().__init__(syncthru, name) self._name = f"{name} Toner {color}" self._color = color - self._unit_of_measurement = UNIT_PERCENTAGE + self._unit_of_measurement = PERCENTAGE self._id_suffix = f"_toner_{color}" def update(self): @@ -186,7 +186,7 @@ class SyncThruDrumSensor(SyncThruSensor): super().__init__(syncthru, name) self._name = f"{name} Drum {color}" self._color = color - self._unit_of_measurement = UNIT_PERCENTAGE + self._unit_of_measurement = PERCENTAGE self._id_suffix = f"_drum_{color}" def update(self): diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index d19e919e41b..163561d13a0 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -8,7 +8,7 @@ from homeassistant.const import ( DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, DATA_TERABYTES, - UNIT_PERCENTAGE, + PERCENTAGE, ) DOMAIN = "synology_dsm" @@ -68,56 +68,56 @@ SECURITY_BINARY_SENSORS = { UTILISATION_SENSORS = { f"{SynoCoreUtilization.API_KEY}:cpu_other_load": { ENTITY_NAME: "CPU Load (Other)", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, f"{SynoCoreUtilization.API_KEY}:cpu_user_load": { ENTITY_NAME: "CPU Load (User)", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, f"{SynoCoreUtilization.API_KEY}:cpu_system_load": { ENTITY_NAME: "CPU Load (System)", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, f"{SynoCoreUtilization.API_KEY}:cpu_total_load": { ENTITY_NAME: "CPU Load (Total)", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, f"{SynoCoreUtilization.API_KEY}:cpu_1min_load": { ENTITY_NAME: "CPU Load (1 min)", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: False, }, f"{SynoCoreUtilization.API_KEY}:cpu_5min_load": { ENTITY_NAME: "CPU Load (5 min)", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, f"{SynoCoreUtilization.API_KEY}:cpu_15min_load": { ENTITY_NAME: "CPU Load (15 min)", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chip", ENTITY_CLASS: None, ENTITY_ENABLE: True, }, f"{SynoCoreUtilization.API_KEY}:memory_real_usage": { ENTITY_NAME: "Memory Usage (Real)", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:memory", ENTITY_CLASS: None, ENTITY_ENABLE: True, @@ -203,7 +203,7 @@ STORAGE_VOL_SENSORS = { }, f"{SynoStorage.API_KEY}:volume_percentage_used": { ENTITY_NAME: "Volume Used", - ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_UNIT: PERCENTAGE, ENTITY_ICON: "mdi:chart-pie", ENTITY_CLASS: None, ENTITY_ENABLE: True, diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index e1293866c1e..e8ff20b2b5a 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -14,10 +14,10 @@ from homeassistant.const import ( DATA_GIBIBYTES, DATA_MEBIBYTES, DATA_RATE_MEGABYTES_PER_SECOND, + PERCENTAGE, STATE_OFF, STATE_ON, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -38,7 +38,7 @@ else: SENSOR_TYPES = { "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None], "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None], - "disk_use_percent": ["Disk use (percent)", UNIT_PERCENTAGE, "mdi:harddisk", None], + "disk_use_percent": ["Disk use (percent)", PERCENTAGE, "mdi:harddisk", None], "ipv4_address": ["IPv4 address", "", "mdi:server-network", None], "ipv6_address": ["IPv6 address", "", "mdi:server-network", None], "last_boot": ["Last boot", "", "mdi:clock", "timestamp"], @@ -47,7 +47,7 @@ SENSOR_TYPES = { "load_5m": ["Load (5m)", " ", "mdi:memory", None], "memory_free": ["Memory free", DATA_MEBIBYTES, "mdi:memory", None], "memory_use": ["Memory use", DATA_MEBIBYTES, "mdi:memory", None], - "memory_use_percent": ["Memory use (percent)", UNIT_PERCENTAGE, "mdi:memory", None], + "memory_use_percent": ["Memory use (percent)", PERCENTAGE, "mdi:memory", None], "network_in": ["Network in", DATA_MEBIBYTES, "mdi:server-network", None], "network_out": ["Network out", DATA_MEBIBYTES, "mdi:server-network", None], "packets_in": ["Packets in", " ", "mdi:server-network", None], @@ -65,11 +65,11 @@ SENSOR_TYPES = { None, ], "process": ["Process", " ", CPU_ICON, None], - "processor_use": ["Processor use", UNIT_PERCENTAGE, CPU_ICON, None], + "processor_use": ["Processor use", PERCENTAGE, CPU_ICON, None], "processor_temperature": ["Processor temperature", TEMP_CELSIUS, CPU_ICON, None], "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None], "swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None], - "swap_use_percent": ["Swap use (percent)", UNIT_PERCENTAGE, "mdi:harddisk", None], + "swap_use_percent": ["Swap use (percent)", PERCENTAGE, "mdi:harddisk", None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 231dc419b6b..56be5eb0123 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -2,7 +2,7 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -145,9 +145,9 @@ class TadoZoneSensor(TadoZoneEntity, Entity): if self.zone_variable == "temperature": return self.hass.config.units.temperature_unit if self.zone_variable == "humidity": - return UNIT_PERCENTAGE + return PERCENTAGE if self.zone_variable == "heating": - return UNIT_PERCENTAGE + return PERCENTAGE if self.zone_variable == "ac": return None diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 8ceb07e1a9f..7b28989ad8e 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from homeassistant.const import ATTR_BATTERY_LEVEL, TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import ATTR_BATTERY_LEVEL, PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -51,7 +51,7 @@ class TahomaSensor(TahomaDevice, Entity): if self.tahoma_device.type == "io:LightIOSystemSensor": return "lx" if self.tahoma_device.type == "Humidity Sensor": - return UNIT_PERCENTAGE + return PERCENTAGE if self.tahoma_device.type == "rtds:RTDSContactSensor": return None if self.tahoma_device.type == "rtds:RTDSMotionSensor": @@ -62,7 +62,7 @@ class TahomaSensor(TahomaDevice, Entity): ): return TEMP_CELSIUS if self.tahoma_device.type == "somfythermostat:SomfyThermostatHumiditySensor": - return UNIT_PERCENTAGE + return PERCENTAGE def update(self): """Update the state.""" diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 5847eecc8a8..ab1cc8b23da 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -8,7 +8,7 @@ from tank_utility import auth, device as tank_monitor import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD, UNIT_PERCENTAGE +from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD, PERCENTAGE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -73,7 +73,7 @@ class TankUtilitySensor(Entity): self._device = device self._state = None self._name = f"Tank Utility {self.device}" - self._unit_of_measurement = UNIT_PERCENTAGE + self._unit_of_measurement = PERCENTAGE self._attributes = {} @property diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index 96331cb5347..4ff2bc84dbe 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import ( CONF_NAME, DATA_GIGABYTES, HTTP_OK, - UNIT_PERCENTAGE, + PERCENTAGE, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -28,7 +28,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) REQUEST_TIMEOUT = 5 # seconds SENSOR_TYPES = { - "usage": ["Usage Ratio", UNIT_PERCENTAGE, "mdi:percent"], + "usage": ["Usage Ratio", PERCENTAGE, "mdi:percent"], "usage_gb": ["Usage", DATA_GIGABYTES, "mdi:download"], "limit": ["Data limit", DATA_GIGABYTES, "mdi:download"], "onpeak_download": ["On Peak Download", DATA_GIGABYTES, "mdi:download"], diff --git a/homeassistant/components/tellduslive/sensor.py b/homeassistant/components/tellduslive/sensor.py index 2e36f8f3ac2..c8f27a9412a 100644 --- a/homeassistant/components/tellduslive/sensor.py +++ b/homeassistant/components/tellduslive/sensor.py @@ -6,11 +6,11 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, POWER_WATT, SPEED_METERS_PER_SECOND, TEMP_CELSIUS, TIME_HOURS, - UNIT_PERCENTAGE, UV_INDEX, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -39,7 +39,7 @@ SENSOR_TYPES = { None, DEVICE_CLASS_TEMPERATURE, ], - SENSOR_TYPE_HUMIDITY: ["Humidity", UNIT_PERCENTAGE, None, DEVICE_CLASS_HUMIDITY], + SENSOR_TYPE_HUMIDITY: ["Humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY], SENSOR_TYPE_RAINRATE: ["Rain rate", f"mm/{TIME_HOURS}", "mdi:water", None], SENSOR_TYPE_RAINTOTAL: ["Rain total", "mm", "mdi:water", None], SENSOR_TYPE_WINDDIRECTION: ["Wind direction", "", "", None], diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 93c510e2fa1..444cabd0180 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -11,8 +11,8 @@ from homeassistant.const import ( CONF_ID, CONF_NAME, CONF_PROTOCOL, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "temperature", config.get(CONF_TEMPERATURE_SCALE) ), tellcore_constants.TELLSTICK_HUMIDITY: DatatypeDescription( - "humidity", UNIT_PERCENTAGE + "humidity", PERCENTAGE ), tellcore_constants.TELLSTICK_RAINRATE: DatatypeDescription("rain rate", ""), tellcore_constants.TELLSTICK_RAINTOTAL: DatatypeDescription("rain total", ""), diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 724ed54e4da..ec8d6a5ef2b 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_HOST, UNIT_PERCENTAGE +from homeassistant.const import CONF_HOST, PERCENTAGE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -17,7 +17,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) SENSOR_TYPES = { - "battery": ["Battery", UNIT_PERCENTAGE, "mdi:battery"], + "battery": ["Battery", PERCENTAGE, "mdi:battery"], "state": ["State", None, None], "capacity": ["Capacity", None, None], } diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index c814134f767..a1afaf9f9b0 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -12,9 +12,9 @@ from homeassistant.const import ( ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) DOMAIN = "toon" @@ -326,7 +326,7 @@ SENSOR_ENTITIES = { ATTR_NAME: "Boiler Modulation Level", ATTR_SECTION: "thermostat", ATTR_MEASUREMENT: "current_modulation_level", - ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:percent", ATTR_DEFAULT_ENABLED: False, @@ -335,7 +335,7 @@ SENSOR_ENTITIES = { ATTR_NAME: "Current Power Usage Covered By Solar", ATTR_SECTION: "power_usage", ATTR_MEASUREMENT: "current_covered_by_solar", - ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:solar-power", ATTR_DEFAULT_ENABLED: True, diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 0cdd9152b4f..e82e352c009 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,6 +1,6 @@ """Support for IKEA Tradfri sensors.""" -from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from .base_class import TradfriBaseDevice from .const import CONF_GATEWAY_ID, DOMAIN, KEY_API, KEY_GATEWAY @@ -48,4 +48,4 @@ class TradfriSensor(TradfriBaseDevice): @property def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" - return UNIT_PERCENTAGE + return PERCENTAGE diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 390f82a26fc..958adfd6915 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -17,9 +17,9 @@ from homeassistant.const import ( DEGREE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, SPEED_METERS_PER_SECOND, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -90,7 +90,7 @@ SENSOR_TYPES = { ], "humidity": [ "Humidity", - UNIT_PERCENTAGE, + PERCENTAGE, "humidity", "mdi:water-percent", DEVICE_CLASS_HUMIDITY, diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index b3a7e8758a0..6f6755a05e7 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -7,8 +7,8 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_proxy=state_proxy, metric_key="A_CYC_FAN_SPEED", device_class=None, - unit_of_measurement=UNIT_PERCENTAGE, + unit_of_measurement=PERCENTAGE, icon="mdi:fan", ), ValloxSensor( @@ -80,7 +80,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_proxy=state_proxy, metric_key="A_CYC_RH_VALUE", device_class=DEVICE_CLASS_HUMIDITY, - unit_of_measurement=UNIT_PERCENTAGE, + unit_of_measurement=PERCENTAGE, icon=None, ), ValloxFilterRemainingSensor( diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 60ebeeb1566..3c4e0974b85 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -7,7 +7,7 @@ import pyvera as veraApi from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, ENTITY_ID_FORMAT from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.util import convert @@ -62,7 +62,7 @@ class VeraSensor(VeraDevice, Entity): if self.vera_device.category == veraApi.CATEGORY_UV_SENSOR: return "level" if self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR: - return UNIT_PERCENTAGE + return PERCENTAGE if self.vera_device.category == veraApi.CATEGORY_POWER_METER: return "watts" diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 384042b7210..ff1c31f7302 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -1,7 +1,7 @@ """Support for Verisure sensors.""" import logging -from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from . import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS, HUB as hub @@ -130,7 +130,7 @@ class VerisureHygrometer(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return UNIT_PERCENTAGE + return PERCENTAGE # pylint: disable=no-self-use def update(self): diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 35ce6dc787b..d54fc6001cc 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -11,9 +11,9 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.helpers.entity import Entity @@ -81,7 +81,7 @@ SENSOR_TYPES = { SENSOR_BURNER_MODULATION: { CONF_NAME: "Burner modulation", CONF_ICON: "mdi:percent", - CONF_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, CONF_GETTER: lambda api: api.getBurnerModulation(), CONF_DEVICE_CLASS: None, }, diff --git a/homeassistant/components/vilfo/const.py b/homeassistant/components/vilfo/const.py index 658d77e1fbc..74eb813bcc5 100644 --- a/homeassistant/components/vilfo/const.py +++ b/homeassistant/components/vilfo/const.py @@ -1,5 +1,5 @@ """Constants for the Vilfo Router integration.""" -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, UNIT_PERCENTAGE +from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE DOMAIN = "vilfo" @@ -21,7 +21,7 @@ ROUTER_MANUFACTURER = "Vilfo AB" SENSOR_TYPES = { ATTR_LOAD: { ATTR_LABEL: "Load", - ATTR_UNIT: UNIT_PERCENTAGE, + ATTR_UNIT: PERCENTAGE, ATTR_ICON: "mdi:memory", ATTR_API_DATA_FIELD: ATTR_API_DATA_FIELD_LOAD, }, diff --git a/homeassistant/components/waterfurnace/sensor.py b/homeassistant/components/waterfurnace/sensor.py index 9378694f5f3..dfb960fe819 100644 --- a/homeassistant/components/waterfurnace/sensor.py +++ b/homeassistant/components/waterfurnace/sensor.py @@ -1,7 +1,7 @@ """Support for Waterfurnace.""" from homeassistant.components.sensor import ENTITY_ID_FORMAT -from homeassistant.const import POWER_WATT, TEMP_FAHRENHEIT, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, POWER_WATT, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.util import slugify @@ -34,10 +34,10 @@ SENSORS = [ "Loop Temp", "enteringwatertemp", "mdi:thermometer", TEMP_FAHRENHEIT ), WFSensorConfig( - "Humidity Set Point", "tstathumidsetpoint", "mdi:water-percent", UNIT_PERCENTAGE + "Humidity Set Point", "tstathumidsetpoint", "mdi:water-percent", PERCENTAGE ), WFSensorConfig( - "Humidity", "tstatrelativehumidity", "mdi:water-percent", UNIT_PERCENTAGE + "Humidity", "tstatrelativehumidity", "mdi:water-percent", PERCENTAGE ), WFSensorConfig("Compressor Power", "compressorpower", "mdi:flash", POWER_WATT), WFSensorConfig("Fan Power", "fanpower", "mdi:flash", POWER_WATT), diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 0b0a6ee519f..702479c4112 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_VOLTAGE, CONF_PASSWORD, CONF_USERNAME, - UNIT_PERCENTAGE, + PERCENTAGE, VOLT, ) import homeassistant.helpers.config_validation as cv @@ -283,5 +283,5 @@ class WirelessTagBaseSensor(Entity): ATTR_VOLTAGE: f"{self._tag.battery_volts:.2f}{VOLT}", ATTR_TAG_SIGNAL_STRENGTH: f"{self._tag.signal_strength}dBm", ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range, - ATTR_TAG_POWER_CONSUMPTION: f"{self._tag.power_consumption:.2f}{UNIT_PERCENTAGE}", + ATTR_TAG_POWER_CONSUMPTION: f"{self._tag.power_consumption:.2f}{PERCENTAGE}", } diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 23c68facb6f..af81c0e68d3 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -30,9 +30,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_WEBHOOK_ID, MASS_KILOGRAMS, + PERCENTAGE, SPEED_METERS_PER_SECOND, TIME_SECONDS, - UNIT_PERCENTAGE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -211,7 +211,7 @@ WITHINGS_ATTRIBUTES = [ Measurement.FAT_RATIO_PCT, MeasureType.FAT_RATIO, "Fat Ratio", - UNIT_PERCENTAGE, + PERCENTAGE, None, SENSOR_DOMAIN, True, @@ -251,7 +251,7 @@ WITHINGS_ATTRIBUTES = [ Measurement.SPO2_PCT, MeasureType.SP02, "SP02", - UNIT_PERCENTAGE, + PERCENTAGE, None, SENSOR_DOMAIN, True, diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 8b53298d576..63a253e6efc 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import ( DATA_BYTES, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TIMESTAMP, - UNIT_PERCENTAGE, + PERCENTAGE, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -171,7 +171,7 @@ class WLEDWifiSignalSensor(WLEDSensor): icon="mdi:wifi", key="wifi_signal", name=f"{coordinator.data.info.name} Wi-Fi Signal", - unit_of_measurement=UNIT_PERCENTAGE, + unit_of_measurement=PERCENTAGE, ) @property diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 8d651800a77..e4fe33f62f1 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -7,7 +7,7 @@ import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT, UNIT_PERCENTAGE +from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT, PERCENTAGE from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -77,7 +77,7 @@ class WorxLandroidSensor(Entity): def unit_of_measurement(self): """Return the unit of measurement of the sensor.""" if self.sensor == "battery": - return UNIT_PERCENTAGE + return PERCENTAGE return None async def async_update(self): diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index d6b7cb447f9..2d9c4f5c9c1 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -23,11 +23,11 @@ from homeassistant.const import ( LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, + PERCENTAGE, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -418,7 +418,7 @@ SENSOR_TYPES = { "Relative Humidity", "conditions", value=lambda wu: int(wu.data["current_observation"]["relative_humidity"][:-1]), - unit_of_measurement=UNIT_PERCENTAGE, + unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", device_class="humidity", ), @@ -926,7 +926,7 @@ SENSOR_TYPES = { 0, "pop", None, - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:umbrella", ), "precip_2d": WUDailySimpleForecastSensorConfig( @@ -934,7 +934,7 @@ SENSOR_TYPES = { 1, "pop", None, - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:umbrella", ), "precip_3d": WUDailySimpleForecastSensorConfig( @@ -942,7 +942,7 @@ SENSOR_TYPES = { 2, "pop", None, - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:umbrella", ), "precip_4d": WUDailySimpleForecastSensorConfig( @@ -950,7 +950,7 @@ SENSOR_TYPES = { 3, "pop", None, - UNIT_PERCENTAGE, + PERCENTAGE, "mdi:umbrella", ), } diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py index a0f6bc69a0d..e6175a4dccf 100644 --- a/homeassistant/components/xbee/__init__.py +++ b/homeassistant/components/xbee/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PIN, EVENT_HOMEASSISTANT_STOP, - UNIT_PERCENTAGE, + PERCENTAGE, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send @@ -421,7 +421,7 @@ class XBeeAnalogIn(Entity): @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return UNIT_PERCENTAGE + return PERCENTAGE def update(self): """Get the latest reading from the ADC.""" diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 471011eab6e..4a6c7ac14fd 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -9,9 +9,9 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, POWER_WATT, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from . import XiaomiDevice @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { "temperature": [TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], - "humidity": [UNIT_PERCENTAGE, None, DEVICE_CLASS_HUMIDITY], + "humidity": [PERCENTAGE, None, DEVICE_CLASS_HUMIDITY], "illumination": ["lm", None, DEVICE_CLASS_ILLUMINANCE], "lux": ["lx", None, DEVICE_CLASS_ILLUMINANCE], "pressure": ["hPa", None, DEVICE_CLASS_PRESSURE], @@ -171,7 +171,7 @@ class XiaomiBatterySensor(XiaomiDevice): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return UNIT_PERCENTAGE + return PERCENTAGE @property def device_class(self): diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 260a1cc8ae7..15dc1bea8bd 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -21,9 +21,9 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, PRESSURE_HPA, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -73,7 +73,7 @@ GATEWAY_SENSOR_TYPES = { unit=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE ), "humidity": SensorType( - unit=UNIT_PERCENTAGE, icon=None, device_class=DEVICE_CLASS_HUMIDITY + unit=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_HUMIDITY ), "pressure": SensorType( unit=PRESSURE_HPA, icon=None, device_class=DEVICE_CLASS_PRESSURE diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index e893c854804..0aa5dea0687 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -19,9 +19,9 @@ from homeassistant.const import ( CONF_NAME, DEGREE, LENGTH_METERS, + PERCENTAGE, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, - UNIT_PERCENTAGE, __version__, ) import homeassistant.helpers.config_validation as cv @@ -43,7 +43,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) SENSOR_TYPES = { "pressure": ("Pressure", "hPa", "LDstat hPa", float), "pressure_sealevel": ("Pressure at Sea Level", "hPa", "LDred hPa", float), - "humidity": ("Humidity", UNIT_PERCENTAGE, "RF %", int), + "humidity": ("Humidity", PERCENTAGE, "RF %", int), "wind_speed": ( "Wind Speed", SPEED_KILOMETERS_PER_HOUR, @@ -58,7 +58,7 @@ SENSOR_TYPES = { float, ), "wind_max_bearing": ("Top Wind Bearing", DEGREE, f"WSR {DEGREE}", int), - "sun_last_hour": ("Sun Last Hour", UNIT_PERCENTAGE, f"SO {UNIT_PERCENTAGE}", int), + "sun_last_hour": ("Sun Last Hour", PERCENTAGE, f"SO {PERCENTAGE}", int), "temperature": ("Temperature", TEMP_CELSIUS, f"T {TEMP_CELSIUS}", float), "precipitation": ("Precipitation", "l/m²", "N l/m²", float), "dewpoint": ("Dew Point", TEMP_CELSIUS, f"TP {TEMP_CELSIUS}", float), diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 38a9f19dce2..9f507275836 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -14,10 +14,10 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, POWER_WATT, STATE_UNKNOWN, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -157,7 +157,7 @@ class Battery(Sensor): SENSOR_ATTR = "battery_percentage_remaining" _device_class = DEVICE_CLASS_BATTERY - _unit = UNIT_PERCENTAGE + _unit = PERCENTAGE @staticmethod def formatter(value): @@ -219,7 +219,7 @@ class Humidity(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_HUMIDITY _divisor = 100 - _unit = UNIT_PERCENTAGE + _unit = PERCENTAGE @STRICT_MATCH(channel_names=CHANNEL_ILLUMINANCE) diff --git a/homeassistant/const.py b/homeassistant/const.py index 09d749c2113..ea5c203ee02 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -444,7 +444,7 @@ CONDUCTIVITY: str = f"µS/{LENGTH_CENTIMETERS}" UV_INDEX: str = "UV index" # Percentage units -UNIT_PERCENTAGE = "%" +PERCENTAGE = "%" # Irradiation units IRRADIATION_WATTS_PER_SQUARE_METER = f"{POWER_WATT}/{AREA_SQUARE_METERS}" diff --git a/tests/components/abode/test_sensor.py b/tests/components/abode/test_sensor.py index 944c1bbeb8c..d99fac50dde 100644 --- a/tests/components/abode/test_sensor.py +++ b/tests/components/abode/test_sensor.py @@ -6,8 +6,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_HUMIDITY, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from .common import setup_platform @@ -32,7 +32,7 @@ async def test_attributes(hass): assert not state.attributes.get("battery_low") assert not state.attributes.get("no_response") assert state.attributes.get("device_type") == "LM" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Environment Sensor Humidity" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 26c7f0d9ab6..b94d17066c8 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -17,11 +17,11 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_TEMPERATURE, LENGTH_METERS, + PERCENTAGE, SPEED_KILOMETERS_PER_HOUR, STATE_UNAVAILABLE, TEMP_CELSIUS, TIME_HOURS, - UNIT_PERCENTAGE, UV_INDEX, ) from homeassistant.setup import async_setup_component @@ -136,7 +136,7 @@ async def test_sensor_with_forecast(hass): assert state.state == "40" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.home_thunderstorm_probability_day_0d") assert entry @@ -147,7 +147,7 @@ async def test_sensor_with_forecast(hass): assert state.state == "40" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-lightning" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE entry = registry.async_get("sensor.home_thunderstorm_probability_night_0d") assert entry @@ -334,7 +334,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "10" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" entry = registry.async_get("sensor.home_cloud_cover") @@ -400,7 +400,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "58" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" entry = registry.async_get("sensor.home_cloud_cover_day_0d") @@ -411,7 +411,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "65" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_ICON) == "mdi:weather-cloudy" entry = registry.async_get("sensor.home_cloud_cover_night_0d") diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 3131789c6e0..45b98d7c27c 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -13,10 +13,10 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, PRESSURE_HPA, STATE_UNAVAILABLE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -35,7 +35,7 @@ async def test_sensor(hass): assert state assert state.state == "92.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY entry = registry.async_get("sensor.home_humidity") diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index e75db4a57dd..85a1d1e315a 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -8,7 +8,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - UNIT_PERCENTAGE, + PERCENTAGE, ) from tests.async_mock import patch @@ -170,7 +170,7 @@ def test_sensor_icon(temperature_sensor): def test_unit_of_measure(default_sensor, battery_sensor): """Test the unit_of_measurement property.""" assert default_sensor.unit_of_measurement is None - assert battery_sensor.unit_of_measurement == UNIT_PERCENTAGE + assert battery_sensor.unit_of_measurement == PERCENTAGE def test_device_class(default_sensor, temperature_sensor, humidity_sensor): diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py index f7e46297532..7e69b59da07 100644 --- a/tests/components/august/test_sensor.py +++ b/tests/components/august/test_sensor.py @@ -1,6 +1,6 @@ """The sensor tests for the august platform.""" -from homeassistant.const import STATE_UNAVAILABLE, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE from tests.components.august.mocks import ( _create_august_with_devices, @@ -21,8 +21,7 @@ async def test_create_doorbell(hass): ) assert sensor_k98gidt45gul_name_battery.state == "96" assert ( - sensor_k98gidt45gul_name_battery.attributes["unit_of_measurement"] - == UNIT_PERCENTAGE + sensor_k98gidt45gul_name_battery.attributes["unit_of_measurement"] == PERCENTAGE ) @@ -34,9 +33,7 @@ async def test_create_doorbell_offline(hass): sensor_tmt100_name_battery = hass.states.get("sensor.tmt100_name_battery") assert sensor_tmt100_name_battery.state == "81" - assert ( - sensor_tmt100_name_battery.attributes["unit_of_measurement"] == UNIT_PERCENTAGE - ) + assert sensor_tmt100_name_battery.attributes["unit_of_measurement"] == PERCENTAGE entry = entity_registry.async_get("sensor.tmt100_name_battery") assert entry @@ -68,7 +65,7 @@ async def test_create_lock_with_linked_keypad(hass): sensor_a6697750d607098bae8d6baa11ef8063_name_battery.attributes[ "unit_of_measurement" ] - == UNIT_PERCENTAGE + == PERCENTAGE ) entry = entity_registry.async_get( "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" @@ -78,7 +75,7 @@ async def test_create_lock_with_linked_keypad(hass): state = hass.states.get("sensor.front_door_lock_keypad_battery") assert state.state == "60" - assert state.attributes["unit_of_measurement"] == UNIT_PERCENTAGE + assert state.attributes["unit_of_measurement"] == PERCENTAGE entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery") assert entry assert entry.unique_id == "5bc65c24e6ef2a263e1450a8_linked_keypad_battery" @@ -98,7 +95,7 @@ async def test_create_lock_with_low_battery_linked_keypad(hass): sensor_a6697750d607098bae8d6baa11ef8063_name_battery.attributes[ "unit_of_measurement" ] - == UNIT_PERCENTAGE + == PERCENTAGE ) entry = entity_registry.async_get( "sensor.a6697750d607098bae8d6baa11ef8063_name_battery" @@ -108,7 +105,7 @@ async def test_create_lock_with_low_battery_linked_keypad(hass): state = hass.states.get("sensor.front_door_lock_keypad_battery") assert state.state == "10" - assert state.attributes["unit_of_measurement"] == UNIT_PERCENTAGE + assert state.attributes["unit_of_measurement"] == PERCENTAGE entry = entity_registry.async_get("sensor.front_door_lock_keypad_battery") assert entry assert entry.unique_id == "5bc65c24e6ef2a263e1450a8_linked_keypad_battery" diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 00c469e3747..4d48959632d 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -20,9 +20,9 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, STATE_UNAVAILABLE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from .const import ( @@ -98,7 +98,7 @@ async def test_awair_gen1_sensors(hass): "sensor.living_room_humidity", f"{AWAIR_UUID}_{SENSOR_TYPES[API_HUMID][ATTR_UNIQUE_ID]}", "41.59", - {ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, "awair_index": 0.0}, + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, "awair_index": 0.0}, ) assert_expected_properties( diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 91109189483..c8dc91ebcf4 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -9,8 +9,8 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, STATE_UNAVAILABLE, - UNIT_PERCENTAGE, ) from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC, utcnow @@ -45,7 +45,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.hl_l2340dw_black_toner_remaining") assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "75" entry = registry.async_get("sensor.hl_l2340dw_black_toner_remaining") @@ -55,7 +55,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.hl_l2340dw_cyan_toner_remaining") assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "10" entry = registry.async_get("sensor.hl_l2340dw_cyan_toner_remaining") @@ -65,7 +65,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.hl_l2340dw_magenta_toner_remaining") assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "8" entry = registry.async_get("sensor.hl_l2340dw_magenta_toner_remaining") @@ -75,7 +75,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.hl_l2340dw_yellow_toner_remaining") assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "2" entry = registry.async_get("sensor.hl_l2340dw_yellow_toner_remaining") @@ -87,7 +87,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_REMAINING_PAGES) == 11014 assert state.attributes.get(ATTR_COUNTER) == 986 - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" entry = registry.async_get("sensor.hl_l2340dw_drum_remaining_life") @@ -99,7 +99,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 assert state.attributes.get(ATTR_COUNTER) == 1611 - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" entry = registry.async_get("sensor.hl_l2340dw_black_drum_remaining_life") @@ -111,7 +111,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 assert state.attributes.get(ATTR_COUNTER) == 1611 - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" entry = registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_life") @@ -123,7 +123,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 assert state.attributes.get(ATTR_COUNTER) == 1611 - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" entry = registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_life") @@ -135,7 +135,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 assert state.attributes.get(ATTR_COUNTER) == 1611 - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "92" entry = registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_life") @@ -145,7 +145,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.hl_l2340dw_fuser_remaining_life") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water-outline" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "97" entry = registry.async_get("sensor.hl_l2340dw_fuser_remaining_life") @@ -155,7 +155,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.hl_l2340dw_belt_unit_remaining_life") assert state assert state.attributes.get(ATTR_ICON) == "mdi:current-ac" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "97" entry = registry.async_get("sensor.hl_l2340dw_belt_unit_remaining_life") @@ -165,7 +165,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.hl_l2340dw_pf_kit_1_remaining_life") assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "98" entry = registry.async_get("sensor.hl_l2340dw_pf_kit_1_remaining_life") diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index d7d16fa5f88..f328fb5a976 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.canary.sensor import ( STATE_AIR_QUALITY_VERY_ABNORMAL, CanarySensor, ) -from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from tests.async_mock import Mock from tests.common import get_test_home_assistant @@ -95,7 +95,7 @@ class TestCanarySensorSetup(unittest.TestCase): sensor.update() assert sensor.name == "Home Family Room Humidity" - assert sensor.unit_of_measurement == UNIT_PERCENTAGE + assert sensor.unit_of_measurement == PERCENTAGE assert sensor.state == 50.46 assert sensor.icon == "mdi:water-percent" @@ -182,7 +182,7 @@ class TestCanarySensorSetup(unittest.TestCase): sensor.update() assert sensor.name == "Home Family Room Battery" - assert sensor.unit_of_measurement == UNIT_PERCENTAGE + assert sensor.unit_of_measurement == PERCENTAGE assert sensor.state == 70.46 assert sensor.icon == "mdi:battery-70" diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index d15826863bb..5a6febc98cf 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -8,11 +8,11 @@ from libpurecool.dyson_pure_cool_link import DysonPureCoolLink from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import sensor as dyson from homeassistant.const import ( + PERCENTAGE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_HOURS, - UNIT_PERCENTAGE, ) from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component @@ -166,7 +166,7 @@ class DysonTest(unittest.TestCase): sensor.entity_id = "sensor.dyson_1" assert not sensor.should_poll assert sensor.state is None - assert sensor.unit_of_measurement == UNIT_PERCENTAGE + assert sensor.unit_of_measurement == PERCENTAGE assert sensor.name == "Device_name Humidity" assert sensor.entity_id == "sensor.dyson_1" @@ -177,7 +177,7 @@ class DysonTest(unittest.TestCase): sensor.entity_id = "sensor.dyson_1" assert not sensor.should_poll assert sensor.state == 45 - assert sensor.unit_of_measurement == UNIT_PERCENTAGE + assert sensor.unit_of_measurement == PERCENTAGE assert sensor.name == "Device_name Humidity" assert sensor.entity_id == "sensor.dyson_1" @@ -188,7 +188,7 @@ class DysonTest(unittest.TestCase): sensor.entity_id = "sensor.dyson_1" assert not sensor.should_poll assert sensor.state == STATE_OFF - assert sensor.unit_of_measurement == UNIT_PERCENTAGE + assert sensor.unit_of_measurement == PERCENTAGE assert sensor.name == "Device_name Humidity" assert sensor.entity_id == "sensor.dyson_1" diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index d2f55fbda16..9b8a81c96d5 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -13,8 +13,8 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR, + PERCENTAGE, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component @@ -44,10 +44,10 @@ async def test_default_setup(hass, aioclient_mock): metrics = { "co2": ["1232.0", CONCENTRATION_PARTS_PER_MILLION], "temperature": ["21.1", TEMP_CELSIUS], - "humidity": ["49.5", UNIT_PERCENTAGE], + "humidity": ["49.5", PERCENTAGE], "pm2_5": ["144.8", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER], "voc": ["340.7", CONCENTRATION_PARTS_PER_BILLION], - "index": ["138.9", UNIT_PERCENTAGE], + "index": ["138.9", PERCENTAGE], } for name, value in metrics.items(): diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 9b687e5dda6..bcbbbf3bcbf 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -24,9 +24,9 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, + PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.core import State @@ -186,7 +186,7 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): "HumiditySensor", "sensor.humidity", "20", - {ATTR_DEVICE_CLASS: "humidity", ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE}, + {ATTR_DEVICE_CLASS: "humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE}, ), ("LightSensor", "sensor.light", "900", {ATTR_DEVICE_CLASS: "illuminance"}), ("LightSensor", "sensor.light", "900", {ATTR_UNIT_OF_MEASUREMENT: "lm"}), diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 94c5c28ecb2..16473cd7b22 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -50,9 +50,9 @@ from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + PERCENTAGE, SERVICE_RELOAD, STATE_ON, - UNIT_PERCENTAGE, ) from homeassistant.core import State from homeassistant.helpers import device_registry @@ -1155,7 +1155,7 @@ async def test_homekit_finds_linked_humidity_sensors( "42", { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ) hass.states.async_set(humidifier.entity_id, STATE_ON) diff --git a/tests/components/homekit/test_type_humidifiers.py b/tests/components/homekit/test_type_humidifiers.py index f9ac65a1942..51f9621d15a 100644 --- a/tests/components/homekit/test_type_humidifiers.py +++ b/tests/components/homekit/test_type_humidifiers.py @@ -32,12 +32,12 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_HUMIDITY, + PERCENTAGE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, - UNIT_PERCENTAGE, ) from tests.common import async_mock_service @@ -314,7 +314,7 @@ async def test_humidifier_with_linked_humidity_sensor(hass, hk_driver): "42.0", { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ) await hass.async_block_till_done() @@ -342,7 +342,7 @@ async def test_humidifier_with_linked_humidity_sensor(hass, hk_driver): "43.0", { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ) await hass.async_block_till_done() @@ -354,7 +354,7 @@ async def test_humidifier_with_linked_humidity_sensor(hass, hk_driver): STATE_UNAVAILABLE, { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 26bb5bfdbad..20029861adb 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -19,10 +19,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, EVENT_HOMEASSISTANT_START, + PERCENTAGE, STATE_OFF, STATE_ON, STATE_UNKNOWN, - UNIT_PERCENTAGE, ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry @@ -163,8 +163,7 @@ async def test_light_brightness(hass, hk_driver, cls, events): assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 assert len(events) == 1 assert ( - events[-1].data[ATTR_VALUE] - == f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}" + events[-1].data[ATTR_VALUE] == f"Set state to 1, brightness at 20{PERCENTAGE}" ) hk_driver.set_characteristics( @@ -186,8 +185,7 @@ async def test_light_brightness(hass, hk_driver, cls, events): assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 assert len(events) == 2 assert ( - events[-1].data[ATTR_VALUE] - == f"Set state to 1, brightness at 40{UNIT_PERCENTAGE}" + events[-1].data[ATTR_VALUE] == f"Set state to 1, brightness at 40{PERCENTAGE}" ) hk_driver.set_characteristics( @@ -207,10 +205,7 @@ async def test_light_brightness(hass, hk_driver, cls, events): assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 3 - assert ( - events[-1].data[ATTR_VALUE] - == f"Set state to 0, brightness at 0{UNIT_PERCENTAGE}" - ) + assert events[-1].data[ATTR_VALUE] == f"Set state to 0, brightness at 0{PERCENTAGE}" # 0 is a special case for homekit, see "Handle Brightness" # in update_state @@ -459,7 +454,7 @@ async def test_light_set_brightness_and_color(hass, hk_driver, cls, events): assert len(events) == 1 assert ( events[-1].data[ATTR_VALUE] - == f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, set color at (145, 75)" + == f"Set state to 1, brightness at 20{PERCENTAGE}, set color at (145, 75)" ) @@ -528,5 +523,5 @@ async def test_light_set_brightness_and_color_temp(hass, hk_driver, cls, events) assert len(events) == 1 assert ( events[-1].data[ATTR_VALUE] - == f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, color temperature at 250" + == f"Set state to 1, brightness at 20{PERCENTAGE}, color temperature at 250" ) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index ec746dec5b9..7ee79352d7b 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, + PERCENTAGE, STATE_HOME, STATE_NOT_HOME, STATE_OFF, @@ -27,7 +28,6 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry @@ -341,7 +341,7 @@ async def test_sensor_restore(hass, hk_driver, events): "12345", suggested_object_id="humidity", device_class="humidity", - unit_of_measurement=UNIT_PERCENTAGE, + unit_of_measurement=PERCENTAGE, ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 2cf7c8dff7b..fe7283b471e 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -24,10 +24,10 @@ from homeassistant.components.homematicip_cloud.sensor import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, POWER_WATT, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.setup import async_setup_component @@ -56,7 +56,7 @@ async def test_hmip_accesspoint_status(hass, default_mock_hap_factory): ) assert hmip_device assert ha_state.state == "8.0" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UNIT_PERCENTAGE + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE await async_manipulate_test_data(hass, hmip_device, "dutyCycle", 17.3) @@ -78,7 +78,7 @@ async def test_hmip_heating_thermostat(hass, default_mock_hap_factory): ) assert ha_state.state == "0" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UNIT_PERCENTAGE + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.37) ha_state = hass.states.get(entity_id) assert ha_state.state == "37" @@ -112,7 +112,7 @@ async def test_hmip_humidity_sensor(hass, default_mock_hap_factory): ) assert ha_state.state == "40" - assert ha_state.attributes["unit_of_measurement"] == UNIT_PERCENTAGE + assert ha_state.attributes["unit_of_measurement"] == PERCENTAGE await async_manipulate_test_data(hass, hmip_device, "humidity", 45) ha_state = hass.states.get(entity_id) assert ha_state.state == "45" diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 973b21a8c89..ca4e56ff54d 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -8,10 +8,10 @@ import homeassistant.components.influxdb as influxdb from homeassistant.components.influxdb.const import DEFAULT_BUCKET from homeassistant.const import ( EVENT_STATE_CHANGED, + PERCENTAGE, STATE_OFF, STATE_ON, STATE_STANDBY, - UNIT_PERCENTAGE, ) from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component @@ -239,7 +239,7 @@ async def test_event_listener( "unit_of_measurement": "foobars", "longitude": "1.1", "latitude": "2.2", - "battery_level": f"99{UNIT_PERCENTAGE}", + "battery_level": f"99{PERCENTAGE}", "temperature": "20c", "last_seen": "Last seen 23 minutes ago", "updated_at": datetime.datetime(2017, 1, 1, 0, 0), @@ -261,7 +261,7 @@ async def test_event_listener( "fields": { "longitude": 1.1, "latitude": 2.2, - "battery_level_str": f"99{UNIT_PERCENTAGE}", + "battery_level_str": f"99{PERCENTAGE}", "battery_level": 99.0, "temperature_str": "20c", "temperature": 20.0, diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 1abf557c93b..1817a66f630 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -3,7 +3,7 @@ from datetime import datetime from homeassistant.components.ipp.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, UNIT_PERCENTAGE +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -43,31 +43,31 @@ async def test_sensors( state = hass.states.get("sensor.epson_xp_6000_series_black_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "58" state = hass.states.get("sensor.epson_xp_6000_series_photo_black_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "98" state = hass.states.get("sensor.epson_xp_6000_series_cyan_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "91" state = hass.states.get("sensor.epson_xp_6000_series_yellow_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "95" state = hass.states.get("sensor.epson_xp_6000_series_magenta_ink") assert state assert state.attributes.get(ATTR_ICON) == "mdi:water" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is PERCENTAGE assert state.state == "73" state = hass.states.get("sensor.epson_xp_6000_series_uptime") diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 52de3dfa1ab..66de0d47b53 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -7,12 +7,12 @@ from homeassistant import config as hass_config from homeassistant.components.min_max import DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.setup import async_setup_component, setup_component @@ -301,7 +301,7 @@ class TestMinMaxSensor(unittest.TestCase): assert state.attributes.get("unit_of_measurement") == "ERR" self.hass.states.set( - entity_ids[2], self.values[2], {ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE} + entity_ids[2], self.values[2], {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} ) self.hass.block_till_done() diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index df1493c084e..1ec97c8c4e2 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -2,7 +2,7 @@ import logging -from homeassistant.const import STATE_UNKNOWN, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import device_registry _LOGGER = logging.getLogger(__name__) @@ -25,7 +25,7 @@ async def test_sensor(hass, create_registrations, webhook_client): "state": 100, "type": "sensor", "unique_id": "battery_state", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, }, }, ) @@ -41,7 +41,7 @@ async def test_sensor(hass, create_registrations, webhook_client): assert entity.attributes["device_class"] == "battery" assert entity.attributes["icon"] == "mdi:battery" - assert entity.attributes["unit_of_measurement"] == UNIT_PERCENTAGE + assert entity.attributes["unit_of_measurement"] == PERCENTAGE assert entity.attributes["foo"] == "bar" assert entity.domain == "sensor" assert entity.name == "Test 1 Battery State" @@ -110,7 +110,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, ca "state": 100, "type": "sensor", "unique_id": "battery_state", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, }, } @@ -129,7 +129,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, ca assert entity.attributes["device_class"] == "battery" assert entity.attributes["icon"] == "mdi:battery" - assert entity.attributes["unit_of_measurement"] == UNIT_PERCENTAGE + assert entity.attributes["unit_of_measurement"] == PERCENTAGE assert entity.attributes["foo"] == "bar" assert entity.domain == "sensor" assert entity.name == "Test 1 Battery State" @@ -150,7 +150,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, ca assert entity.attributes["device_class"] == "battery" assert entity.attributes["icon"] == "mdi:battery" - assert entity.attributes["unit_of_measurement"] == UNIT_PERCENTAGE + assert entity.attributes["unit_of_measurement"] == PERCENTAGE assert entity.attributes["foo"] == "bar" assert entity.domain == "sensor" assert entity.name == "Test 1 Battery State" diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index 423c728ff72..a67873fd4e4 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -8,9 +8,9 @@ from homeassistant.components.mold_indicator.sensor import ( import homeassistant.components.sensor as sensor from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, STATE_UNKNOWN, TEMP_CELSIUS, - UNIT_PERCENTAGE, ) from homeassistant.setup import setup_component @@ -30,7 +30,7 @@ class TestSensorMoldIndicator(unittest.TestCase): "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} ) self.hass.states.set( - "test.indoorhumidity", "50", {ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE} + "test.indoorhumidity", "50", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} ) self.addCleanup(self.tear_down_cleanup) @@ -56,7 +56,7 @@ class TestSensorMoldIndicator(unittest.TestCase): self.hass.block_till_done() moldind = self.hass.states.get("sensor.mold_indicator") assert moldind - assert UNIT_PERCENTAGE == moldind.attributes.get("unit_of_measurement") + assert PERCENTAGE == moldind.attributes.get("unit_of_measurement") def test_invalidcalib(self): """Test invalid sensor values.""" @@ -67,7 +67,7 @@ class TestSensorMoldIndicator(unittest.TestCase): "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} ) self.hass.states.set( - "test.indoorhumidity", "0", {ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE} + "test.indoorhumidity", "0", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} ) assert setup_component( @@ -101,7 +101,7 @@ class TestSensorMoldIndicator(unittest.TestCase): "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} ) self.hass.states.set( - "test.indoorhumidity", "-1", {ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE} + "test.indoorhumidity", "-1", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} ) assert setup_component( @@ -128,7 +128,7 @@ class TestSensorMoldIndicator(unittest.TestCase): assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None self.hass.states.set( - "test.indoorhumidity", "A", {ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE} + "test.indoorhumidity", "A", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} ) self.hass.block_till_done() moldind = self.hass.states.get("sensor.mold_indicator") @@ -232,7 +232,7 @@ class TestSensorMoldIndicator(unittest.TestCase): self.hass.states.set( "test.indoorhumidity", STATE_UNKNOWN, - {ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE}, + {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE}, ) self.hass.block_till_done() moldind = self.hass.states.get("sensor.mold_indicator") @@ -242,7 +242,7 @@ class TestSensorMoldIndicator(unittest.TestCase): assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None self.hass.states.set( - "test.indoorhumidity", "20", {ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE} + "test.indoorhumidity", "20", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} ) self.hass.block_till_done() moldind = self.hass.states.get("sensor.mold_indicator") @@ -289,7 +289,7 @@ class TestSensorMoldIndicator(unittest.TestCase): assert self.hass.states.get("sensor.mold_indicator").state == "57" self.hass.states.set( - "test.indoorhumidity", "20", {ATTR_UNIT_OF_MEASUREMENT: UNIT_PERCENTAGE} + "test.indoorhumidity", "20", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} ) self.hass.block_till_done() assert self.hass.states.get("sensor.mold_indicator").state == "23" diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py index a0fa387cd43..289947fbdf6 100644 --- a/tests/components/nexia/test_sensor.py +++ b/tests/components/nexia/test_sensor.py @@ -1,6 +1,6 @@ """The sensor tests for the nexia platform.""" -from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from .util import async_init_integration @@ -69,7 +69,7 @@ async def test_create_sensors(hass): expected_attributes = { "attribution": "Data provided by mynexia.com", "friendly_name": "Master Suite Current Compressor Speed", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -99,7 +99,7 @@ async def test_create_sensors(hass): "attribution": "Data provided by mynexia.com", "device_class": "humidity", "friendly_name": "Master Suite Relative Humidity", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -113,7 +113,7 @@ async def test_create_sensors(hass): expected_attributes = { "attribution": "Data provided by mynexia.com", "friendly_name": "Master Suite Requested Compressor Speed", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index 0c6887288b9..42fdc09e2b1 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -1,6 +1,6 @@ """The sensor tests for the nut platform.""" -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from .util import async_init_integration @@ -21,7 +21,7 @@ async def test_pr3000rt2u(hass): "device_class": "battery", "friendly_name": "Ups1 Battery Charge", "state": "Online", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -47,7 +47,7 @@ async def test_cp1350c(hass): "device_class": "battery", "friendly_name": "Ups1 Battery Charge", "state": "Online", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -152,7 +152,7 @@ async def test_cp1500pfclcd(hass): "device_class": "battery", "friendly_name": "Ups1 Battery Charge", "state": "Online", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -177,7 +177,7 @@ async def test_dl650elcd(hass): "device_class": "battery", "friendly_name": "Ups1 Battery Charge", "state": "Online", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -202,7 +202,7 @@ async def test_blazer_usb(hass): "device_class": "battery", "friendly_name": "Ups1 Battery Charge", "state": "Online", - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index d007b664633..0be7c12c5a8 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -1,7 +1,7 @@ """The sensor tests for the powerwall platform.""" from homeassistant.components.powerwall.const import DOMAIN -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.setup import async_setup_component from .mocks import _mock_get_config, _mock_powerwall_with_fixtures @@ -104,7 +104,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.powerwall_charge") assert state.state == "47" expected_attributes = { - "unit_of_measurement": UNIT_PERCENTAGE, + "unit_of_measurement": PERCENTAGE, "friendly_name": "Powerwall Charge", "device_class": "battery", } diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index 77f911f0ed0..1468037b70d 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -12,7 +12,7 @@ from homeassistant.components.rflink import ( EVENT_KEY_SENSOR, TMP_ENTITY, ) -from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, STATE_UNKNOWN, TEMP_CELSIUS from tests.components.rflink.test_init import mock_rflink @@ -151,7 +151,7 @@ async def test_aliases(hass, monkeypatch): "id": "test_alias_02_0", "sensor": "humidity", "value": 65, - "unit": UNIT_PERCENTAGE, + "unit": PERCENTAGE, } ) await hass.async_block_till_done() @@ -160,7 +160,7 @@ async def test_aliases(hass, monkeypatch): updated_sensor = hass.states.get("sensor.test_02") assert updated_sensor assert updated_sensor.state == "65" - assert updated_sensor.attributes["unit_of_measurement"] == UNIT_PERCENTAGE + assert updated_sensor.attributes["unit_of_measurement"] == PERCENTAGE async def test_race_condition(hass, monkeypatch): diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index d87cf1a71e2..18239550a85 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -2,7 +2,7 @@ import pytest from homeassistant.components.rfxtrx.const import ATTR_EVENT -from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import State from homeassistant.setup import async_setup_component @@ -81,7 +81,7 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Humidity" - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE state = hass.states.get(f"{base_id}_humidity_status") assert state @@ -99,7 +99,7 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Battery numeric" - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE async def test_several_sensors(hass, rfxtrx): @@ -145,7 +145,7 @@ async def test_several_sensors(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 06:01 Humidity" ) - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE async def test_discover_sensor(hass, rfxtrx_automatic): @@ -159,7 +159,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_humidity") assert state assert state.state == "27" - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE state = hass.states.get(f"{base_id}_humidity_status") assert state @@ -179,7 +179,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_battery_numeric") assert state assert state.state == "90" - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE # 2 await rfxtrx.signal("0a52080405020095240279") @@ -188,7 +188,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state assert state.state == "36" - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE state = hass.states.get(f"{base_id}_humidity_status") assert state @@ -208,7 +208,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_battery_numeric") assert state assert state.state == "90" - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE # 1 Update await rfxtrx.signal("0a52085e070100b31b0279") @@ -217,7 +217,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_humidity") assert state assert state.state == "27" - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE state = hass.states.get(f"{base_id}_humidity_status") assert state @@ -237,7 +237,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_battery_numeric") assert state assert state.state == "90" - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE assert len(hass.states.async_all()) == 10 diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index f92a28d358b..0c0c9c6c22b 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -4,7 +4,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS -from homeassistant.const import CONF_PLATFORM, STATE_UNKNOWN, UNIT_PERCENTAGE +from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -99,13 +99,13 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): expected_capabilities = { "extra_fields": [ { - "description": {"suffix": UNIT_PERCENTAGE}, + "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, "type": "float", }, { - "description": {"suffix": UNIT_PERCENTAGE}, + "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, "type": "float", diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 9bdaf5b1fe0..dda57de0d9d 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -6,7 +6,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS -from homeassistant.const import CONF_PLATFORM, STATE_UNKNOWN, UNIT_PERCENTAGE +from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -104,13 +104,13 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): expected_capabilities = { "extra_fields": [ { - "description": {"suffix": UNIT_PERCENTAGE}, + "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, "type": "float", }, { - "description": {"suffix": UNIT_PERCENTAGE}, + "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, "type": "float", diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 6d2b73d787d..b9669d0c8ed 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -12,8 +12,8 @@ from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHING from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, STATE_UNKNOWN, - UNIT_PERCENTAGE, ) from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -38,7 +38,7 @@ async def test_entity_state(hass, device_factory): await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) state = hass.states.get("sensor.sensor_1_battery") assert state.state == "100" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UNIT_PERCENTAGE + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE assert state.attributes[ATTR_FRIENDLY_NAME] == f"{device.label} Battery" diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index 891adac91ae..b831de56b05 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components.spaceapi import DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI -from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -64,7 +64,7 @@ SENSOR_OUTPUT = { {"location": "Home", "name": "temp2", "unit": TEMP_CELSIUS, "value": "23"}, ], "humidity": [ - {"location": "Home", "name": "hum1", "unit": UNIT_PERCENTAGE, "value": "88"} + {"location": "Home", "name": "hum1", "unit": PERCENTAGE, "value": "88"} ], } @@ -82,7 +82,7 @@ def mock_client(hass, hass_client): "test.temp2", 23, attributes={"unit_of_measurement": TEMP_CELSIUS} ) hass.states.async_set( - "test.hum1", 88, attributes={"unit_of_measurement": UNIT_PERCENTAGE} + "test.hum1", 88, attributes={"unit_of_measurement": PERCENTAGE} ) return hass.loop.run_until_complete(hass_client()) diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index 302df0492c8..e1d658d05b7 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -1,7 +1,7 @@ """Tests for the Start.ca sensor platform.""" from homeassistant.bootstrap import async_setup_component from homeassistant.components.startca.sensor import StartcaData -from homeassistant.const import DATA_GIGABYTES, HTTP_NOT_FOUND, UNIT_PERCENTAGE +from homeassistant.const import DATA_GIGABYTES, HTTP_NOT_FOUND, PERCENTAGE from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -53,7 +53,7 @@ async def test_capped_setup(hass, aioclient_mock): await hass.async_block_till_done() state = hass.states.get("sensor.start_ca_usage_ratio") - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE assert state.state == "76.24" state = hass.states.get("sensor.start_ca_usage") @@ -149,7 +149,7 @@ async def test_unlimited_setup(hass, aioclient_mock): await hass.async_block_till_done() state = hass.states.get("sensor.start_ca_usage_ratio") - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE assert state.state == "0" state = hass.states.get("sensor.start_ca_usage") diff --git a/tests/components/teksavvy/test_sensor.py b/tests/components/teksavvy/test_sensor.py index 7de8651f16b..b0de95d72d1 100644 --- a/tests/components/teksavvy/test_sensor.py +++ b/tests/components/teksavvy/test_sensor.py @@ -1,7 +1,7 @@ """Tests for the TekSavvy sensor platform.""" from homeassistant.bootstrap import async_setup_component from homeassistant.components.teksavvy.sensor import TekSavvyData -from homeassistant.const import DATA_GIGABYTES, HTTP_NOT_FOUND, UNIT_PERCENTAGE +from homeassistant.const import DATA_GIGABYTES, HTTP_NOT_FOUND, PERCENTAGE from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -75,7 +75,7 @@ async def test_capped_setup(hass, aioclient_mock): assert state.state == "235.57" state = hass.states.get("sensor.teksavvy_usage_ratio") - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE assert state.state == "56.69" state = hass.states.get("sensor.teksavvy_usage") @@ -161,7 +161,7 @@ async def test_unlimited_setup(hass, aioclient_mock): assert state.state == "226.75" state = hass.states.get("sensor.teksavvy_usage_ratio") - assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE + assert state.attributes.get("unit_of_measurement") == PERCENTAGE assert state.state == "0" state = hass.states.get("sensor.teksavvy_remaining") diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index cb50ad82789..36730e8d6d2 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -3,7 +3,7 @@ from typing import Any, Callable, Tuple import pyvera as pv -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config @@ -115,7 +115,7 @@ async def test_humidity_sensor( category=pv.CATEGORY_HUMIDITY_SENSOR, class_property="humidity", assert_states=(("12", "12"), ("13", "13")), - assert_unit_of_measurement=UNIT_PERCENTAGE, + assert_unit_of_measurement=PERCENTAGE, ) diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index adbd2e6e1fc..f4efea1b57d 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DATA_BYTES, - UNIT_PERCENTAGE, + PERCENTAGE, ) from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -128,7 +128,7 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_wifi_signal") assert state assert state.attributes.get(ATTR_ICON) == "mdi:wifi" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "76" entry = registry.async_get("sensor.wled_rgb_light_wifi_signal") diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 89d3d270322..7cba51b3e5c 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -14,12 +14,12 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, + PERCENTAGE, POWER_WATT, STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) from homeassistant.helpers import restore_state from homeassistant.util import dt as dt_util @@ -36,7 +36,7 @@ from .common import ( async def async_test_humidity(hass, cluster, entity_id): """Test humidity sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 100}) - assert_state(hass, entity_id, "10.0", UNIT_PERCENTAGE) + assert_state(hass, entity_id, "10.0", PERCENTAGE) async def async_test_temperature(hass, cluster, entity_id): diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index 817a43d4f58..b2cd895df37 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -153,12 +153,12 @@ def test_alarm_sensor_value_changed(mock_openzwave): node = MockNode( command_classes=[const.COMMAND_CLASS_ALARM, const.COMMAND_CLASS_SENSOR_ALARM] ) - value = MockValue(data=12.34, node=node, units=homeassistant.const.UNIT_PERCENTAGE) + value = MockValue(data=12.34, node=node, units=homeassistant.const.PERCENTAGE) values = MockEntityValues(primary=value) device = sensor.get_device(node=node, values=values, node_config={}) assert device.state == 12.34 - assert device.unit_of_measurement == homeassistant.const.UNIT_PERCENTAGE + assert device.unit_of_measurement == homeassistant.const.PERCENTAGE value.data = 45.67 value_changed(value) assert device.state == 45.67 diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 43939f44e7e..7e923e6abc0 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -5,7 +5,7 @@ import logging import pytest -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import entity_platform, entity_registry @@ -838,7 +838,7 @@ async def test_entity_info_added_to_entity_registry(hass): capability_attributes={"max": 100}, supported_features=5, device_class="mock-device-class", - unit_of_measurement=UNIT_PERCENTAGE, + unit_of_measurement=PERCENTAGE, ) await component.async_add_entities([entity_default]) @@ -850,7 +850,7 @@ async def test_entity_info_added_to_entity_registry(hass): assert entry_default.capabilities == {"max": 100} assert entry_default.supported_features == 5 assert entry_default.device_class == "mock-device-class" - assert entry_default.unit_of_measurement == UNIT_PERCENTAGE + assert entry_default.unit_of_measurement == PERCENTAGE async def test_override_restored_entities(hass): diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index b81cc34c34b..8d0cdf6ac93 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -4,7 +4,7 @@ Provide a mock sensor platform. Call init before using it in your tests to ensure clean test data. """ import homeassistant.components.sensor as sensor -from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.const import PERCENTAGE from tests.common import MockEntity @@ -12,8 +12,8 @@ DEVICE_CLASSES = list(sensor.DEVICE_CLASSES) DEVICE_CLASSES.append("none") UNITS_OF_MEASUREMENT = { - sensor.DEVICE_CLASS_BATTERY: UNIT_PERCENTAGE, # % of battery that is left - sensor.DEVICE_CLASS_HUMIDITY: UNIT_PERCENTAGE, # % of humidity in the air + sensor.DEVICE_CLASS_BATTERY: PERCENTAGE, # % of battery that is left + sensor.DEVICE_CLASS_HUMIDITY: PERCENTAGE, # % of humidity in the air sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # signal strength (dB/dBm) sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) From 958c9c08d61b0f3420319186b5bbf1d36dbdcb4d Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sat, 5 Sep 2020 15:11:15 -0400 Subject: [PATCH 667/862] Use more homeassistant constants in NWS (#39690) --- homeassistant/components/nws/const.py | 43 +++++++++++++++++-------- homeassistant/components/nws/weather.py | 10 +++--- tests/components/nws/const.py | 11 ++++--- tests/components/nws/test_weather.py | 33 ++++++++++--------- 4 files changed, 58 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index c8798134473..574ad6925ac 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -1,4 +1,20 @@ """Constants for National Weather Service Integration.""" +from homeassistant.components.weather import ( + ATTR_CONDITION_CLOUDY, + ATTR_CONDITION_EXCEPTIONAL, + ATTR_CONDITION_FOG, + ATTR_CONDITION_HAIL, + ATTR_CONDITION_LIGHTNING, + ATTR_CONDITION_LIGHTNING_RAINY, + ATTR_CONDITION_PARTLYCLOUDY, + ATTR_CONDITION_POURING, + ATTR_CONDITION_RAINY, + ATTR_CONDITION_SNOWY, + ATTR_CONDITION_SNOWY_RAINY, + ATTR_CONDITION_WINDY, + ATTR_CONDITION_WINDY_VARIANT, +) + DOMAIN = "nws" CONF_STATION = "station" @@ -6,11 +22,10 @@ CONF_STATION = "station" ATTRIBUTION = "Data from National Weather Service/NOAA" ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" -ATTR_FORECAST_PRECIP_PROB = "precipitation_probability" ATTR_FORECAST_DAYTIME = "daytime" CONDITION_CLASSES = { - "exceptional": [ + ATTR_CONDITION_EXCEPTIONAL: [ "Tornado", "Hurricane conditions", "Tropical storm conditions", @@ -20,37 +35,37 @@ CONDITION_CLASSES = { "Hot", "Cold", ], - "snowy": ["Snow", "Sleet", "Blizzard"], - "snowy-rainy": [ + ATTR_CONDITION_SNOWY: ["Snow", "Sleet", "Blizzard"], + ATTR_CONDITION_SNOWY_RAINY: [ "Rain/snow", "Rain/sleet", "Freezing rain/snow", "Freezing rain", "Rain/freezing rain", ], - "hail": [], - "lightning-rainy": [ + ATTR_CONDITION_HAIL: [], + ATTR_CONDITION_LIGHTNING_RAINY: [ "Thunderstorm (high cloud cover)", "Thunderstorm (medium cloud cover)", "Thunderstorm (low cloud cover)", ], - "lightning": [], - "pouring": [], - "rainy": [ + ATTR_CONDITION_LIGHTNING: [], + ATTR_CONDITION_POURING: [], + ATTR_CONDITION_RAINY: [ "Rain", "Rain showers (high cloud cover)", "Rain showers (low cloud cover)", ], - "windy-variant": ["Mostly cloudy and windy", "Overcast and windy"], - "windy": [ + ATTR_CONDITION_WINDY_VARIANT: ["Mostly cloudy and windy", "Overcast and windy"], + ATTR_CONDITION_WINDY: [ "Fair/clear and windy", "A few clouds and windy", "Partly cloudy and windy", ], - "fog": ["Fog/mist"], + ATTR_CONDITION_FOG: ["Fog/mist"], "clear": ["Fair/clear"], # sunny and clear-night - "cloudy": ["Mostly cloudy", "Overcast"], - "partlycloudy": ["A few clouds", "Partly cloudy"], + ATTR_CONDITION_CLOUDY: ["Mostly cloudy", "Overcast"], + ATTR_CONDITION_PARTLYCLOUDY: ["A few clouds", "Partly cloudy"], } DAYNIGHT = "daynight" diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index f7890190490..3c641447e84 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -3,7 +3,10 @@ from datetime import timedelta import logging from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, + ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, @@ -33,7 +36,6 @@ from . import base_unique_id from .const import ( ATTR_FORECAST_DAYTIME, ATTR_FORECAST_DETAILED_DESCRIPTION, - ATTR_FORECAST_PRECIP_PROB, ATTRIBUTION, CONDITION_CLASSES, COORDINATOR_FORECAST, @@ -75,9 +77,9 @@ def convert_condition(time, weather): if cond == "clear": if time == "day": - return "sunny", max(prec_probs) + return ATTR_CONDITION_SUNNY, max(prec_probs) if time == "night": - return "clear-night", max(prec_probs) + return ATTR_CONDITION_CLEAR_NIGHT, max(prec_probs) return cond, max(prec_probs) @@ -267,7 +269,7 @@ class NWSWeather(WeatherEntity): else: cond, precip = None, None data[ATTR_FORECAST_CONDITION] = cond - data[ATTR_FORECAST_PRECIP_PROB] = precip + data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = precip data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") wind_speed = forecast_entry.get("windSpeedAvg") diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 8b23f9cc850..ae2f826294f 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -1,8 +1,9 @@ """Helpers for interacting with pynws.""" from homeassistant.components.nws.const import CONF_STATION -from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB from homeassistant.components.weather import ( + ATTR_CONDITION_LIGHTNING_RAINY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, @@ -101,23 +102,23 @@ DEFAULT_FORECAST = [ ] EXPECTED_FORECAST_IMPERIAL = { - ATTR_FORECAST_CONDITION: "lightning-rainy", + ATTR_FORECAST_CONDITION: ATTR_CONDITION_LIGHTNING_RAINY, ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", ATTR_FORECAST_TEMP: 10, ATTR_FORECAST_WIND_SPEED: 10, ATTR_FORECAST_WIND_BEARING: 180, - ATTR_FORECAST_PRECIP_PROB: 90, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, } EXPECTED_FORECAST_METRIC = { - ATTR_FORECAST_CONDITION: "lightning-rainy", + ATTR_FORECAST_CONDITION: ATTR_CONDITION_LIGHTNING_RAINY, ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", ATTR_FORECAST_TEMP: round(convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS)), ATTR_FORECAST_WIND_SPEED: round( convert_distance(10, LENGTH_MILES, LENGTH_KILOMETERS) ), ATTR_FORECAST_WIND_BEARING: 180, - ATTR_FORECAST_PRECIP_PROB: 90, + ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, } NONE_FORECAST = [{key: None for key in DEFAULT_FORECAST[0]}] diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 06053302ec7..2604c6f39ac 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -5,7 +5,8 @@ import aiohttp import pytest from homeassistant.components import nws -from homeassistant.components.weather import ATTR_FORECAST +from homeassistant.components.weather import ATTR_CONDITION_SUNNY, ATTR_FORECAST +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM @@ -46,7 +47,7 @@ async def test_imperial_metric( state = hass.states.get("weather.abc_hourly") assert state - assert state.state == "sunny" + assert state.state == ATTR_CONDITION_SUNNY data = state.attributes for key, value in result_observation.items(): @@ -59,7 +60,7 @@ async def test_imperial_metric( state = hass.states.get("weather.abc_daynight") assert state - assert state.state == "sunny" + assert state.state == ATTR_CONDITION_SUNNY data = state.attributes for key, value in result_observation.items(): @@ -85,7 +86,7 @@ async def test_none_values(hass, mock_simple_nws): await hass.async_block_till_done() state = hass.states.get("weather.abc_daynight") - assert state.state == "unknown" + assert state.state == STATE_UNKNOWN data = state.attributes for key in EXPECTED_OBSERVATION_IMPERIAL: assert data.get(key) is None @@ -111,7 +112,7 @@ async def test_none(hass, mock_simple_nws): state = hass.states.get("weather.abc_daynight") assert state - assert state.state == "unknown" + assert state.state == STATE_UNKNOWN data = state.attributes for key in EXPECTED_OBSERVATION_IMPERIAL: @@ -198,11 +199,11 @@ async def test_error_observation(hass, mock_simple_nws): state = hass.states.get("weather.abc_daynight") assert state - assert state.state == "unavailable" + assert state.state == STATE_UNAVAILABLE state = hass.states.get("weather.abc_hourly") assert state - assert state.state == "unavailable" + assert state.state == STATE_UNAVAILABLE # second update happens faster and succeeds instance.update_observation.side_effect = None @@ -213,7 +214,7 @@ async def test_error_observation(hass, mock_simple_nws): state = hass.states.get("weather.abc_daynight") assert state - assert state.state == "sunny" + assert state.state == ATTR_CONDITION_SUNNY state = hass.states.get("weather.abc_hourly") assert state @@ -229,11 +230,11 @@ async def test_error_observation(hass, mock_simple_nws): state = hass.states.get("weather.abc_daynight") assert state - assert state.state == "sunny" + assert state.state == ATTR_CONDITION_SUNNY state = hass.states.get("weather.abc_hourly") assert state - assert state.state == "sunny" + assert state.state == ATTR_CONDITION_SUNNY # after 20 minutes data caching expires, data is no longer shown increment_time(timedelta(minutes=10)) @@ -241,11 +242,11 @@ async def test_error_observation(hass, mock_simple_nws): state = hass.states.get("weather.abc_daynight") assert state - assert state.state == "unavailable" + assert state.state == STATE_UNAVAILABLE state = hass.states.get("weather.abc_hourly") assert state - assert state.state == "unavailable" + assert state.state == STATE_UNAVAILABLE async def test_error_forecast(hass, mock_simple_nws): @@ -265,7 +266,7 @@ async def test_error_forecast(hass, mock_simple_nws): state = hass.states.get("weather.abc_daynight") assert state - assert state.state == "unavailable" + assert state.state == STATE_UNAVAILABLE instance.update_forecast.side_effect = None @@ -276,7 +277,7 @@ async def test_error_forecast(hass, mock_simple_nws): state = hass.states.get("weather.abc_daynight") assert state - assert state.state == "sunny" + assert state.state == ATTR_CONDITION_SUNNY async def test_error_forecast_hourly(hass, mock_simple_nws): @@ -294,7 +295,7 @@ async def test_error_forecast_hourly(hass, mock_simple_nws): state = hass.states.get("weather.abc_hourly") assert state - assert state.state == "unavailable" + assert state.state == STATE_UNAVAILABLE instance.update_forecast_hourly.assert_called_once() @@ -307,4 +308,4 @@ async def test_error_forecast_hourly(hass, mock_simple_nws): state = hass.states.get("weather.abc_hourly") assert state - assert state.state == "sunny" + assert state.state == ATTR_CONDITION_SUNNY From 29c1bec0f3cd6d258c92a0aec6147ff5587b1d54 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 5 Sep 2020 21:49:33 +0200 Subject: [PATCH 668/862] Match documentation on expose being default True (#39692) --- homeassistant/components/google_assistant/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index b1182b436b5..0afd66c10aa 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__) ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_EXPOSE): cv.boolean, + vol.Optional(CONF_EXPOSE, default=True): cv.boolean, vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_ROOM_HINT): cv.string, } From b6630a48b2881d0dd37d0229379bc05dfa0fc07b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 5 Sep 2020 23:02:32 +0200 Subject: [PATCH 669/862] Add tradfri api call error handling (#39681) Co-authored-by: Franck Nijhof --- homeassistant/components/tradfri/__init__.py | 10 +- .../components/tradfri/base_class.py | 17 +- homeassistant/components/tradfri/const.py | 3 +- homeassistant/components/tradfri/cover.py | 6 +- homeassistant/components/tradfri/light.py | 10 +- homeassistant/components/tradfri/sensor.py | 14 +- homeassistant/components/tradfri/switch.py | 6 +- tests/components/tradfri/test_light.py | 151 ++++++++++-------- 8 files changed, 124 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index d3caaf54762..002231e4e20 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -24,9 +24,10 @@ from .const import ( CONF_KEY, CONFIG_FILE, DEFAULT_ALLOW_TRADFRI_GROUPS, + DEVICES, DOMAIN, + GROUPS, KEY_API, - KEY_GATEWAY, PLATFORMS, ) @@ -116,13 +117,18 @@ async def async_setup_entry(hass, entry): try: gateway_info = await api(gateway.get_gateway_info()) + devices_commands = await api(gateway.get_devices()) + devices = await api(devices_commands) + groups_commands = await api(gateway.get_groups()) + groups = await api(groups_commands) except RequestError as err: await factory.shutdown() raise ConfigEntryNotReady from err tradfri_data[KEY_API] = api - tradfri_data[KEY_GATEWAY] = gateway tradfri_data[FACTORY] = factory + tradfri_data[DEVICES] = devices + tradfri_data[GROUPS] = groups dev_reg = await hass.helpers.device_registry.async_get_registry() dev_reg.async_get_or_create( diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 0850bec6c9b..0c9f2f7312f 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -1,4 +1,5 @@ """Base class for IKEA TRADFRI.""" +from functools import wraps import logging from pytradfri.error import PytradfriError @@ -11,6 +12,20 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +def handle_error(func): + """Handle tradfri api call error.""" + + @wraps(func) + async def wrapper(command): + """Decorate api call.""" + try: + await func(command) + except PytradfriError as err: + _LOGGER.error("Unable to execute command %s: %s", command, err) + + return wrapper + + class TradfriBaseClass(Entity): """Base class for IKEA TRADFRI. @@ -19,7 +34,7 @@ class TradfriBaseClass(Entity): def __init__(self, device, api, gateway_id): """Initialize a device.""" - self._api = api + self._api = handle_error(api) self._device = None self._device_control = None self._device_data = None diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 423620ecb2b..f7c2bf6cbe5 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -19,7 +19,8 @@ CONFIG_FILE = ".tradfri_psk.conf" DEFAULT_ALLOW_TRADFRI_GROUPS = False DOMAIN = "tradfri" KEY_API = "tradfri_api" -KEY_GATEWAY = "tradfri_gateway" +DEVICES = "tradfri_devices" +GROUPS = "tradfri_groups" KEY_SECURITY_CODE = "security_code" SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index cab7b6bbab7..2d99de7756a 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -3,7 +3,7 @@ from homeassistant.components.cover import ATTR_POSITION, CoverEntity from .base_class import TradfriBaseDevice -from .const import ATTR_MODEL, CONF_GATEWAY_ID, DOMAIN, KEY_API, KEY_GATEWAY +from .const import ATTR_MODEL, CONF_GATEWAY_ID, DEVICES, DOMAIN, KEY_API async def async_setup_entry(hass, config_entry, async_add_entities): @@ -11,10 +11,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway_id = config_entry.data[CONF_GATEWAY_ID] tradfri_data = hass.data[DOMAIN][config_entry.entry_id] api = tradfri_data[KEY_API] - gateway = tradfri_data[KEY_GATEWAY] + devices = tradfri_data[DEVICES] - devices_commands = await api(gateway.get_devices()) - devices = await api(devices_commands) covers = [dev for dev in devices if dev.has_blind_control] if covers: async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers) diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 29e096b2c49..939968852d9 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -21,9 +21,10 @@ from .const import ( ATTR_TRANSITION_TIME, CONF_GATEWAY_ID, CONF_IMPORT_GROUPS, + DEVICES, DOMAIN, + GROUPS, KEY_API, - KEY_GATEWAY, SUPPORTED_GROUP_FEATURES, SUPPORTED_LIGHT_FEATURES, ) @@ -36,17 +37,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway_id = config_entry.data[CONF_GATEWAY_ID] tradfri_data = hass.data[DOMAIN][config_entry.entry_id] api = tradfri_data[KEY_API] - gateway = tradfri_data[KEY_GATEWAY] + devices = tradfri_data[DEVICES] - devices_commands = await api(gateway.get_devices()) - devices = await api(devices_commands) lights = [dev for dev in devices if dev.has_light_control] if lights: async_add_entities(TradfriLight(light, api, gateway_id) for light in lights) if config_entry.data[CONF_IMPORT_GROUPS]: - groups_commands = await api(gateway.get_groups()) - groups = await api(groups_commands) + groups = tradfri_data[GROUPS] if groups: async_add_entities(TradfriGroup(group, api, gateway_id) for group in groups) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index e82e352c009..c2bf640e2aa 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -3,7 +3,7 @@ from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from .base_class import TradfriBaseDevice -from .const import CONF_GATEWAY_ID, DOMAIN, KEY_API, KEY_GATEWAY +from .const import CONF_GATEWAY_ID, DEVICES, DOMAIN, KEY_API async def async_setup_entry(hass, config_entry, async_add_entities): @@ -11,20 +11,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway_id = config_entry.data[CONF_GATEWAY_ID] tradfri_data = hass.data[DOMAIN][config_entry.entry_id] api = tradfri_data[KEY_API] - gateway = tradfri_data[KEY_GATEWAY] + devices = tradfri_data[DEVICES] - devices_commands = await api(gateway.get_devices()) - all_devices = await api(devices_commands) - devices = ( + sensors = ( dev - for dev in all_devices + for dev in devices if not dev.has_light_control and not dev.has_socket_control and not dev.has_blind_control and not dev.has_signal_repeater_control ) - if devices: - async_add_entities(TradfriSensor(device, api, gateway_id) for device in devices) + if sensors: + async_add_entities(TradfriSensor(sensor, api, gateway_id) for sensor in sensors) class TradfriSensor(TradfriBaseDevice): diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 5bc5e6ab8e8..6634090d00d 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -2,7 +2,7 @@ from homeassistant.components.switch import SwitchEntity from .base_class import TradfriBaseDevice -from .const import CONF_GATEWAY_ID, DOMAIN, KEY_API, KEY_GATEWAY +from .const import CONF_GATEWAY_ID, DEVICES, DOMAIN, KEY_API async def async_setup_entry(hass, config_entry, async_add_entities): @@ -10,10 +10,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway_id = config_entry.data[CONF_GATEWAY_ID] tradfri_data = hass.data[DOMAIN][config_entry.entry_id] api = tradfri_data[KEY_API] - gateway = tradfri_data[KEY_GATEWAY] + devices = tradfri_data[DEVICES] - devices_commands = await api(gateway.get_devices()) - devices = await api(devices_commands) switches = [dev for dev in devices if dev.has_socket_control] if switches: async_add_entities( diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index b4c209c1493..653a9ce62df 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -100,7 +100,7 @@ async def generate_psk(self, code): return "mock" -async def setup_gateway(hass, mock_gateway, mock_api): +async def setup_integration(hass): """Load the Tradfri platform with a mock gateway.""" entry = MockConfigEntry( domain=tradfri.DOMAIN, @@ -112,43 +112,44 @@ async def setup_gateway(hass, mock_gateway, mock_api): "gateway_id": MOCK_GATEWAY_ID, }, ) - tradfri_data = {} - hass.data[tradfri.DOMAIN] = {entry.entry_id: tradfri_data} - tradfri_data[tradfri.KEY_API] = mock_api - tradfri_data[tradfri.KEY_GATEWAY] = mock_gateway - await hass.config_entries.async_forward_entry_setup(entry, "light") + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() -def mock_light(test_features={}, test_state={}, n=0): +def mock_light(test_features=None, test_state=None, light_number=0): """Mock a tradfri light.""" + if test_features is None: + test_features = {} + if test_state is None: + test_state = {} mock_light_data = Mock(**test_state) dev_info_mock = MagicMock() dev_info_mock.manufacturer = "manufacturer" dev_info_mock.model_number = "model" dev_info_mock.firmware_version = "1.2.3" - mock_light = Mock( - id=f"mock-light-id-{n}", + _mock_light = Mock( + id=f"mock-light-id-{light_number}", reachable=True, observe=Mock(), device_info=dev_info_mock, ) - mock_light.name = f"tradfri_light_{n}" + _mock_light.name = f"tradfri_light_{light_number}" # Set supported features for the light. features = {**DEFAULT_TEST_FEATURES, **test_features} - lc = LightControl(mock_light) - for k, v in features.items(): - setattr(lc, k, v) + light_control = LightControl(_mock_light) + for attr, value in features.items(): + setattr(light_control, attr, value) # Store the initial state. - setattr(lc, "lights", [mock_light_data]) - mock_light.light_control = lc - return mock_light + setattr(light_control, "lights", [mock_light_data]) + _mock_light.light_control = light_control + return _mock_light -async def test_light(hass, mock_gateway, mock_api): +async def test_light(hass, mock_gateway, api_factory): """Test that lights are correctly added.""" features = {"can_set_dimmer": True, "can_set_color": True, "can_set_temp": True} @@ -162,7 +163,7 @@ async def test_light(hass, mock_gateway, mock_api): mock_gateway.mock_devices.append( mock_light(test_features=features, test_state=state) ) - await setup_gateway(hass, mock_gateway, mock_api) + await setup_integration(hass) lamp_1 = hass.states.get("light.tradfri_light_0") assert lamp_1 is not None @@ -171,48 +172,60 @@ async def test_light(hass, mock_gateway, mock_api): assert lamp_1.attributes["hs_color"] == (0.549, 0.153) -async def test_light_observed(hass, mock_gateway, mock_api): +async def test_light_observed(hass, mock_gateway, api_factory): """Test that lights are correctly observed.""" light = mock_light() mock_gateway.mock_devices.append(light) - await setup_gateway(hass, mock_gateway, mock_api) + await setup_integration(hass) assert len(light.observe.mock_calls) > 0 -async def test_light_available(hass, mock_gateway, mock_api): +async def test_light_available(hass, mock_gateway, api_factory): """Test light available property.""" - light = mock_light({"state": True}, n=1) + light = mock_light({"state": True}, light_number=1) light.reachable = True - light2 = mock_light({"state": True}, n=2) + light2 = mock_light({"state": True}, light_number=2) light2.reachable = False mock_gateway.mock_devices.append(light) mock_gateway.mock_devices.append(light2) - await setup_gateway(hass, mock_gateway, mock_api) + await setup_integration(hass) assert hass.states.get("light.tradfri_light_1").state == "on" assert hass.states.get("light.tradfri_light_2").state == "unavailable" -# Combine TURN_ON_TEST_CASES and TRANSITION_CASES_FOR_TESTS -ALL_TURN_ON_TEST_CASES = [["test_features", "test_data", "expected_result", "id"], []] +def create_all_turn_on_cases(): + """Create all turn on test cases.""" + # Combine TURN_ON_TEST_CASES and TRANSITION_CASES_FOR_TESTS + all_turn_on_test_cases = [ + ["test_features", "test_data", "expected_result", "device_id"], + [], + ] + index = 1 + for test_case in TURN_ON_TEST_CASES: + for trans in TRANSITION_CASES_FOR_TESTS: + case = deepcopy(test_case) + if trans is not None: + case[1]["transition"] = trans + case.append(index) + index += 1 + all_turn_on_test_cases[1].append(case) -idx = 1 -for tc in TURN_ON_TEST_CASES: - for trans in TRANSITION_CASES_FOR_TESTS: - case = deepcopy(tc) - if trans is not None: - case[1]["transition"] = trans - case.append(idx) - idx = idx + 1 - ALL_TURN_ON_TEST_CASES[1].append(case) + return all_turn_on_test_cases -@pytest.mark.parametrize(*ALL_TURN_ON_TEST_CASES) +@pytest.mark.parametrize(*create_all_turn_on_cases()) async def test_turn_on( - hass, mock_gateway, mock_api, test_features, test_data, expected_result, id + hass, + mock_gateway, + api_factory, + test_features, + test_data, + expected_result, + device_id, ): """Test turning on a light.""" # Note pytradfri style, not hass. Values not really important. @@ -224,15 +237,17 @@ async def test_turn_on( } # Setup the gateway with a mock light. - light = mock_light(test_features=test_features, test_state=initial_state, n=id) + light = mock_light( + test_features=test_features, test_state=initial_state, light_number=device_id + ) mock_gateway.mock_devices.append(light) - await setup_gateway(hass, mock_gateway, mock_api) + await setup_integration(hass) # Use the turn_on service call to change the light state. await hass.services.async_call( "light", "turn_on", - {"entity_id": f"light.tradfri_light_{id}", **test_data}, + {"entity_id": f"light.tradfri_light_{device_id}", **test_data}, blocking=True, ) await hass.async_block_till_done() @@ -243,39 +258,39 @@ async def test_turn_on( _, callkwargs = mock_func.call_args assert "callback" in callkwargs # Callback function to refresh light state. - cb = callkwargs["callback"] + callback = callkwargs["callback"] responses = mock_gateway.mock_responses # State on command data. data = {"3311": [{"5850": 1}]} # Add data for all sent commands. - for r in responses: - data["3311"][0] = {**data["3311"][0], **r["3311"][0]} + for resp in responses: + data["3311"][0] = {**data["3311"][0], **resp["3311"][0]} # Use the callback function to update the light state. dev = Device(data) light_data = Light(dev, 0) light.light_control.lights[0] = light_data - cb(light) + callback(light) await hass.async_block_till_done() # Check that the state is correct. - states = hass.states.get(f"light.tradfri_light_{id}") - for k, v in expected_result.items(): - if k == "state": - assert states.state == v + states = hass.states.get(f"light.tradfri_light_{device_id}") + for result, value in expected_result.items(): + if result == "state": + assert states.state == value else: # Allow some rounding error in color conversions. - assert states.attributes[k] == pytest.approx(v, abs=0.01) + assert states.attributes[result] == pytest.approx(value, abs=0.01) -async def test_turn_off(hass, mock_gateway, mock_api): +async def test_turn_off(hass, mock_gateway, api_factory): """Test turning off a light.""" state = {"state": True, "dimmer": 100} light = mock_light(test_state=state) mock_gateway.mock_devices.append(light) - await setup_gateway(hass, mock_gateway, mock_api) + await setup_integration(hass) # Use the turn_off service call to change the light state. await hass.services.async_call( @@ -289,19 +304,19 @@ async def test_turn_off(hass, mock_gateway, mock_api): _, callkwargs = mock_func.call_args assert "callback" in callkwargs # Callback function to refresh light state. - cb = callkwargs["callback"] + callback = callkwargs["callback"] responses = mock_gateway.mock_responses data = {"3311": [{}]} # Add data for all sent commands. - for r in responses: - data["3311"][0] = {**data["3311"][0], **r["3311"][0]} + for resp in responses: + data["3311"][0] = {**data["3311"][0], **resp["3311"][0]} # Use the callback function to update the light state. dev = Device(data) light_data = Light(dev, 0) light.light_control.lights[0] = light_data - cb(light) + callback(light) await hass.async_block_till_done() # Check that the state is correct. @@ -309,23 +324,25 @@ async def test_turn_off(hass, mock_gateway, mock_api): assert states.state == "off" -def mock_group(test_state={}, n=0): +def mock_group(test_state=None, group_number=0): """Mock a Tradfri group.""" + if test_state is None: + test_state = {} default_state = {"state": False, "dimmer": 0} state = {**default_state, **test_state} - mock_group = Mock(member_ids=[], observe=Mock(), **state) - mock_group.name = f"tradfri_group_{n}" - return mock_group + _mock_group = Mock(member_ids=[], observe=Mock(), **state) + _mock_group.name = f"tradfri_group_{group_number}" + return _mock_group -async def test_group(hass, mock_gateway, mock_api): +async def test_group(hass, mock_gateway, api_factory): """Test that groups are correctly added.""" mock_gateway.mock_groups.append(mock_group()) state = {"state": True, "dimmer": 100} mock_gateway.mock_groups.append(mock_group(state, 1)) - await setup_gateway(hass, mock_gateway, mock_api) + await setup_integration(hass) group = hass.states.get("light.tradfri_group_0") assert group is not None @@ -337,15 +354,15 @@ async def test_group(hass, mock_gateway, mock_api): assert group.attributes["brightness"] == 100 -async def test_group_turn_on(hass, mock_gateway, mock_api): +async def test_group_turn_on(hass, mock_gateway, api_factory): """Test turning on a group.""" group = mock_group() - group2 = mock_group(n=1) - group3 = mock_group(n=2) + group2 = mock_group(group_number=1) + group3 = mock_group(group_number=2) mock_gateway.mock_groups.append(group) mock_gateway.mock_groups.append(group2) mock_gateway.mock_groups.append(group3) - await setup_gateway(hass, mock_gateway, mock_api) + await setup_integration(hass) # Use the turn_off service call to change the light state. await hass.services.async_call( @@ -370,11 +387,11 @@ async def test_group_turn_on(hass, mock_gateway, mock_api): group3.set_dimmer.assert_called_with(100, transition_time=10) -async def test_group_turn_off(hass, mock_gateway, mock_api): +async def test_group_turn_off(hass, mock_gateway, api_factory): """Test turning off a group.""" group = mock_group({"state": True}) mock_gateway.mock_groups.append(group) - await setup_gateway(hass, mock_gateway, mock_api) + await setup_integration(hass) # Use the turn_off service call to change the light state. await hass.services.async_call( From 74fea6d306d7dea48a60d8bc7d0014d14c0eccef Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 6 Sep 2020 00:10:18 +0200 Subject: [PATCH 670/862] Add Arcam radio media browsing (#39593) --- .../components/arcam_fmj/manifest.json | 2 +- .../components/arcam_fmj/media_player.py | 59 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/arcam_fmj/test_media_player.py | 2 +- 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index fb66687611c..5f8b8bb69a2 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -3,7 +3,7 @@ "name": "Arcam FMJ Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", - "requirements": ["arcam-fmj==0.5.2"], + "requirements": ["arcam-fmj==0.5.3"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 6b5b5856190..17607cd3f8c 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -8,6 +8,8 @@ from homeassistant import config_entries from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, + SUPPORT_BROWSE_MEDIA, + SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -16,6 +18,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) +from homeassistant.components.media_player.errors import BrowseError from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType @@ -72,6 +75,8 @@ class ArcamFmj(MediaPlayerEntity): self._uuid = uuid self._support = ( SUPPORT_SELECT_SOURCE + | SUPPORT_PLAY_MEDIA + | SUPPORT_BROWSE_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP @@ -238,6 +243,45 @@ class ArcamFmj(MediaPlayerEntity): """Turn the media player off.""" await self._state.set_power(False) + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + if media_content_id not in (None, "root"): + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) + + presets = self._state.get_preset_details() + + radio = [ + { + "title": preset.name, + "media_content_id": f"preset:{preset.index}", + "media_content_type": MEDIA_TYPE_MUSIC, + "can_play": True, + } + for preset in presets.values() + ] + + root = { + "title": "Root", + "media_content_id": "root", + "media_content_type": "library", + "can_play": False, + "children": radio, + } + + return root + + async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: + """Play media.""" + + if media_id.startswith("preset:"): + preset = int(media_id[7:]) + await self._state.set_tuner_preset(preset) + else: + _LOGGER.error("Media %s is not supported", media_id) + return + @property def source(self): """Return the current input source.""" @@ -303,6 +347,21 @@ class ArcamFmj(MediaPlayerEntity): value = None return value + @property + def media_content_id(self): + """Content type of current playing media.""" + source = self._state.get_source() + if source in (SourceCodes.DAB, SourceCodes.FM): + preset = self._state.get_tuner_preset() + if preset: + value = f"preset:{preset}" + else: + value = None + else: + value = None + + return value + @property def media_channel(self): """Channel currently playing.""" diff --git a/requirements_all.txt b/requirements_all.txt index 46f40e086f8..aede49805ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -272,7 +272,7 @@ aprslib==0.6.46 aqualogic==1.0 # homeassistant.components.arcam_fmj -arcam-fmj==0.5.2 +arcam-fmj==0.5.3 # homeassistant.components.arris_tg2492lg arris-tg2492lg==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f571c3b220..a1b327a7cf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ apprise==0.8.8 aprslib==0.6.46 # homeassistant.components.arcam_fmj -arcam-fmj==0.5.2 +arcam-fmj==0.5.3 # homeassistant.components.dlna_dmr # homeassistant.components.upnp diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index d6c219a6d96..91117cff0a2 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -62,7 +62,7 @@ async def test_powered_on(player, state): async def test_supported_features(player, state): """Test supported features.""" data = await update(player) - assert data.attributes["supported_features"] == 69004 + assert data.attributes["supported_features"] == 200588 async def test_turn_on(player, state): From 1fffa748e548b5bbe946165619df52b940e4a913 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 6 Sep 2020 00:17:10 +0200 Subject: [PATCH 671/862] Improve handling of DHCP devices (#39683) --- homeassistant/components/shelly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index ebc3d287e5e..8c83d9ee6e9 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -69,7 +69,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): super().__init__( hass, _LOGGER, - name=device.settings["name"] or entry.title, + name=device.settings["name"] or device.settings["device"]["hostname"], update_interval=timedelta(seconds=5), ) self.hass = hass From f584e3f689abe776e3584833a1c0415c73184834 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 6 Sep 2020 00:36:32 +0200 Subject: [PATCH 672/862] Bump pytradfri to 7.0.1 (#39696) --- homeassistant/components/tradfri/manifest.json | 12 +++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 05cfdcdfeee..334f6246508 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,9 +3,15 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.0"], + "requirements": [ + "pytradfri[async]==7.0.1" + ], "homekit": { - "models": ["TRADFRI"] + "models": [ + "TRADFRI" + ] }, - "codeowners": ["@ggravlingen"] + "codeowners": [ + "@ggravlingen" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index aede49805ef..044c3fb0db7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1805,7 +1805,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.0 +pytradfri[async]==7.0.1 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1b327a7cf6..cba38c5477b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -851,7 +851,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.0 +pytradfri[async]==7.0.1 # homeassistant.components.vera pyvera==0.3.9 From 2f0138e891a2ec3c53e2766868e6f74515568823 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 6 Sep 2020 00:03:25 +0000 Subject: [PATCH 673/862] [ci skip] Translation update --- .../components/atag/translations/pl.json | 2 +- .../components/blink/translations/pl.json | 2 +- .../components/broadlink/translations/pl.json | 46 +++++++++++++++++++ .../components/denonavr/translations/pl.json | 2 +- .../components/gogogate2/translations/en.json | 2 +- .../components/gogogate2/translations/pl.json | 2 +- .../components/gogogate2/translations/ru.json | 2 +- .../components/group/translations/pl.json | 4 +- .../homematicip_cloud/translations/pl.json | 1 + .../components/insteon/translations/pl.json | 30 +++++++++++- .../components/kodi/translations/pl.json | 7 +++ .../components/lock/translations/pl.json | 4 +- .../components/nzbget/translations/pl.json | 8 ++++ .../openweathermap/translations/cs.json | 35 ++++++++++++++ .../openweathermap/translations/pl.json | 31 +++++++++++++ .../openweathermap/translations/ru.json | 35 ++++++++++++++ .../openweathermap/translations/zh-Hant.json | 35 ++++++++++++++ .../ovo_energy/translations/pl.json | 7 +++ .../components/pi_hole/translations/pl.json | 2 +- .../components/sharkiq/translations/pl.json | 8 ++++ .../components/spotify/translations/pl.json | 3 ++ .../components/weather/translations/pl.json | 14 +++--- .../components/withings/translations/pl.json | 2 +- .../xiaomi_aqara/translations/pl.json | 4 +- .../xiaomi_miio/translations/pl.json | 2 +- .../components/yeelight/translations/pl.json | 40 ++++++++++++++++ .../components/zwave/translations/pl.json | 4 +- 27 files changed, 309 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/broadlink/translations/pl.json create mode 100644 homeassistant/components/kodi/translations/pl.json create mode 100644 homeassistant/components/openweathermap/translations/cs.json create mode 100644 homeassistant/components/openweathermap/translations/pl.json create mode 100644 homeassistant/components/openweathermap/translations/ru.json create mode 100644 homeassistant/components/openweathermap/translations/zh-Hant.json create mode 100644 homeassistant/components/ovo_energy/translations/pl.json create mode 100644 homeassistant/components/yeelight/translations/pl.json diff --git a/homeassistant/components/atag/translations/pl.json b/homeassistant/components/atag/translations/pl.json index 971ff7221b9..d7d633aeda5 100644 --- a/homeassistant/components/atag/translations/pl.json +++ b/homeassistant/components/atag/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Do Home Assistant mo\u017cna doda\u0107 tylko jedno urz\u0105dzenie Atag." + "already_configured": "To urz\u0105dzenie zosta\u0142o ju\u017c dodane do Home Assistant" }, "error": { "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie." diff --git a/homeassistant/components/blink/translations/pl.json b/homeassistant/components/blink/translations/pl.json index 99e3f9aebd9..7d6d01266d9 100644 --- a/homeassistant/components/blink/translations/pl.json +++ b/homeassistant/components/blink/translations/pl.json @@ -12,7 +12,7 @@ "data": { "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego" }, - "description": "Wpisz kod PIN wys\u0142any na Tw\u00f3j adres e-mail. Je\u015bli wiadomo\u015b\u0107 e-mail nie zawiera kodu PIN, pozostaw pole puste.", + "description": "Wpisz kod PIN wys\u0142any na Tw\u00f3j adres e-mail. Je\u015bli.", "title": "Uwierzytelnianie dwusk\u0142adnikowe" }, "user": { diff --git a/homeassistant/components/broadlink/translations/pl.json b/homeassistant/components/broadlink/translations/pl.json new file mode 100644 index 00000000000..a168d020d02 --- /dev/null +++ b/homeassistant/components/broadlink/translations/pl.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja integracji Broadlink jest ju\u017c w toku.", + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem", + "invalid_host": "B\u0142\u0119dna nazwa hosta b\u0105d\u017a adres IP", + "unknown": "Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d" + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem", + "invalid_host": "B\u0142\u0119dna nazwa hosta b\u0105d\u017a adres IP", + "unknown": "Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name} ({model}, IP: {host})", + "step": { + "auth": { + "title": "Autoryzuj urz\u0105dzeniem" + }, + "finish": { + "data": { + "name": "Nazwa" + }, + "title": "Wprowad\u017a nazw\u0119 dla urz\u0105dzenia" + }, + "reset": { + "description": "Twoje urz\u0105dzenie jest zablokowane. Post\u0119puj zgodnie z instrukcjami, aby je odblokowa\u0107:\\n1. Zresetuj urz\u0105dzenie do ustawie\u0144 fabrycznych.\\n2. Dodaj urz\u0105dzenie w oficjalnej aplikacji do swojej sieci.\\n3. Stop! Nie ko\u0144cz konfiguracji w aplikacji tylko j\u0105 zamknij.\\n4. Potwierd\u017a odblokowanie (przycisk Odblokuj).", + "title": "Odblokuj urz\u0105dzeniem" + }, + "unlock": { + "data": { + "unlock": "Odblokuj" + }, + "description": "To urz\u0105dzenie jest zablokowane. Mo\u017ce to prowadzi\u0107 do problem\u00f3w z uwierzytelnianiem w Home Assistant. Chcesz je odblokowa\u0107?", + "title": "Odblokuj urz\u0105dzenie (opcjonalne)" + }, + "user": { + "data": { + "host": "Host lub Adres IP", + "timeout": "Timeout (limit czasu)" + }, + "title": "\u0141\u0105czenie z urz\u0105dzeniem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/pl.json b/homeassistant/components/denonavr/translations/pl.json index d8fd08f01f7..f025f6e2dd4 100644 --- a/homeassistant/components/denonavr/translations/pl.json +++ b/homeassistant/components/denonavr/translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem, spr\u00f3buj ponownie, od\u0142\u0105czenie zasilania sieciowego i kabla Ethernet i ponowne pod\u0142\u0105czenie mo\u017ce pom\u00f3c", "not_denonavr_manufacturer": "Nie jest to urz\u0105dzenie AVR firmy Denon, producent wykrytego urz\u0105dzenia nie pasuje.", "not_denonavr_missing": "Nie jest to urz\u0105dzenie AVR firmy Denon, dane z automatycznego wykrywania nie s\u0105 kompletne." }, diff --git a/homeassistant/components/gogogate2/translations/en.json b/homeassistant/components/gogogate2/translations/en.json index d5a93091d91..b39bdfd7bb7 100644 --- a/homeassistant/components/gogogate2/translations/en.json +++ b/homeassistant/components/gogogate2/translations/en.json @@ -15,7 +15,7 @@ "username": "Username" }, "description": "Provide requisite information below.", - "title": "Setup GogoGate2" + "title": "Setup GogoGate2 or iSmartGate" } } } diff --git a/homeassistant/components/gogogate2/translations/pl.json b/homeassistant/components/gogogate2/translations/pl.json index 84a683b4dbd..7a6c33be781 100644 --- a/homeassistant/components/gogogate2/translations/pl.json +++ b/homeassistant/components/gogogate2/translations/pl.json @@ -15,7 +15,7 @@ "username": "Nazwa u\u017cytkownika" }, "description": "Wprowad\u017a wymagane informacje poni\u017cej.", - "title": "Konfiguracja GogoGate2" + "title": "Konfiguracja GogoGate2 oraz iSmartGate" } } } diff --git a/homeassistant/components/gogogate2/translations/ru.json b/homeassistant/components/gogogate2/translations/ru.json index f7f88db1d1b..0c8f14f65f4 100644 --- a/homeassistant/components/gogogate2/translations/ru.json +++ b/homeassistant/components/gogogate2/translations/ru.json @@ -15,7 +15,7 @@ "username": "\u041b\u043e\u0433\u0438\u043d" }, "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 GogoGate2.", - "title": "GogoGate2" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 GogoGate2 \u0438\u043b\u0438 iSmartGate" } } } diff --git a/homeassistant/components/group/translations/pl.json b/homeassistant/components/group/translations/pl.json index b72716973d8..b82492d83d0 100644 --- a/homeassistant/components/group/translations/pl.json +++ b/homeassistant/components/group/translations/pl.json @@ -3,14 +3,14 @@ "_": { "closed": "zamkni\u0119te", "home": "w domu", - "locked": "zamkni\u0119ty", + "locked": "Zablokowane", "not_home": "poza domem", "off": "wy\u0142\u0105czony", "ok": "ok", "on": "w\u0142\u0105czony", "open": "otwarte", "problem": "problem", - "unlocked": "otwarty" + "unlocked": "Odblokowany" } }, "title": "Grupa" diff --git a/homeassistant/components/homematicip_cloud/translations/pl.json b/homeassistant/components/homematicip_cloud/translations/pl.json index 9071c2072ae..cfd8e96c2a2 100644 --- a/homeassistant/components/homematicip_cloud/translations/pl.json +++ b/homeassistant/components/homematicip_cloud/translations/pl.json @@ -6,6 +6,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { + "invalid_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie.", "invalid_sgtin_or_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie.", "press_the_button": "Prosz\u0119 nacisn\u0105\u0107 niebieski przycisk.", "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107, spr\u00f3buj ponownie.", diff --git a/homeassistant/components/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json index baa541c74f2..1fe0f6b7451 100644 --- a/homeassistant/components/insteon/translations/pl.json +++ b/homeassistant/components/insteon/translations/pl.json @@ -1,11 +1,39 @@ { + "config": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + }, + "step": { + "hubv2": { + "data": { + "host": "Adres IP", + "username": "Nazwa u\u017cytkownika" + } + }, + "plm": { + "data": { + "device": "\u015acie\u017cka urz\u0105dzenia USB" + } + }, + "user": { + "title": "Insteon" + } + } + }, "options": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + }, "step": { "change_hub_config": { "data": { "host": "Adres IP", "password": "Has\u0142o", - "port": "Port" + "port": "Port", + "username": "Nazwa u\u017cytkownika" } } } diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json new file mode 100644 index 00000000000..d9590796ce5 --- /dev/null +++ b/homeassistant/components/kodi/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/translations/pl.json b/homeassistant/components/lock/translations/pl.json index 3bdada017ff..d67adc8c9a1 100644 --- a/homeassistant/components/lock/translations/pl.json +++ b/homeassistant/components/lock/translations/pl.json @@ -16,8 +16,8 @@ }, "state": { "_": { - "locked": "zamkni\u0119ty", - "unlocked": "otwarty" + "locked": "Zablokowany", + "unlocked": "Odblokowanie" } }, "title": "Zamek" diff --git a/homeassistant/components/nzbget/translations/pl.json b/homeassistant/components/nzbget/translations/pl.json index 599616a8b15..121f77b0a96 100644 --- a/homeassistant/components/nzbget/translations/pl.json +++ b/homeassistant/components/nzbget/translations/pl.json @@ -2,6 +2,14 @@ "config": { "error": { "invalid_auth": "Niepoprawne uwierzytelnienie." + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "name": "Nazwa" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/cs.json b/homeassistant/components/openweathermap/translations/cs.json new file mode 100644 index 00000000000..4e18283006a --- /dev/null +++ b/homeassistant/components/openweathermap/translations/cs.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Integrace OpenWeatherMap pro tyto sou\u0159adnice je ji\u017e nakonfigurov\u00e1na." + }, + "error": { + "auth": "Kl\u00ed\u010d API nen\u00ed spr\u00e1vn\u00fd.", + "connection": "Nelze se p\u0159ipojit k OpenWeatherMap API" + }, + "step": { + "user": { + "data": { + "api_key": "API kl\u00ed\u010d OpenWeatherMap", + "language": "Jazyk", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "mode": "Re\u017eim", + "name": "N\u00e1zev integrace" + }, + "description": "Nastaven\u00ed integrace OpenWeatherMap. Chcete-li vygenerovat API kl\u00ed\u010d, p\u0159ejd\u011bte na https://openweathermap.org/appid", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Jazyk", + "mode": "Re\u017eim" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/pl.json b/homeassistant/components/openweathermap/translations/pl.json new file mode 100644 index 00000000000..74e74422bd2 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/pl.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "auth": "Klucz API jest nieprawid\u0142owy.", + "connection": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z OWM" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API OpenWeatherMap", + "language": "J\u0119zyk", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "mode": "Tryb", + "name": "Nazwa integracji" + }, + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "J\u0119zyk", + "mode": "Tryb" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/ru.json b/homeassistant/components/openweathermap/translations/ru.json new file mode 100644 index 00000000000..a79e51f6053 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/ru.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenWeatherMap \u0441 \u0442\u0430\u043a\u0438\u043c\u0438 \u0436\u0435 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." + }, + "error": { + "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "connection": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API OpenWeatherMap." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "language": "\u042f\u0437\u044b\u043a", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "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 OpenWeatherMap. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://openweathermap.org/appid.", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u042f\u0437\u044b\u043a", + "mode": "\u0420\u0435\u0436\u0438\u043c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/zh-Hant.json b/homeassistant/components/openweathermap/translations/zh-Hant.json new file mode 100644 index 00000000000..e1ed3c9f89d --- /dev/null +++ b/homeassistant/components/openweathermap/translations/zh-Hant.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64 OpenWeatherMap \u6574\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", + "connection": "\u7121\u6cd5\u9023\u7dda\u81f3 OWM API" + }, + "step": { + "user": { + "data": { + "api_key": "OpenWeatherMap API \u5bc6\u9470", + "language": "\u8a9e\u8a00", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "mode": "\u6a21\u5f0f", + "name": "\u6574\u5408\u540d\u7a31" + }, + "description": "\u6b32\u8a2d\u5b9a OpenWeatherMap \u6574\u5408\u3002\u8acb\u81f3 https://openweathermap.org/appid \u7522\u751f API \u5bc6\u9470", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u8a9e\u8a00", + "mode": "\u6a21\u5f0f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json new file mode 100644 index 00000000000..42afe86d48a --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index aa35fc9d918..b974e6c04c9 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "Klucz API (opcjonalnie)", + "api_key": "Klucz API", "host": "Nazwa hosta lub adres IP", "location": "Lokalizacja", "name": "Nazwa", diff --git a/homeassistant/components/sharkiq/translations/pl.json b/homeassistant/components/sharkiq/translations/pl.json index 599616a8b15..c32bd13fe16 100644 --- a/homeassistant/components/sharkiq/translations/pl.json +++ b/homeassistant/components/sharkiq/translations/pl.json @@ -2,6 +2,14 @@ "config": { "error": { "invalid_auth": "Niepoprawne uwierzytelnienie." + }, + "step": { + "reauth": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json index 9dbd73661ee..0ed5a24ce16 100644 --- a/homeassistant/components/spotify/translations/pl.json +++ b/homeassistant/components/spotify/translations/pl.json @@ -11,6 +11,9 @@ "step": { "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "title": "Ponownie uwierzytelnij ze Spotify" } } } diff --git a/homeassistant/components/weather/translations/pl.json b/homeassistant/components/weather/translations/pl.json index c7d387690ca..8dbd5bd3b3a 100644 --- a/homeassistant/components/weather/translations/pl.json +++ b/homeassistant/components/weather/translations/pl.json @@ -9,13 +9,13 @@ "lightning": "b\u0142yskawice", "lightning-rainy": "burza", "partlycloudy": "cz\u0119\u015bciowe zachmurzenie", - "pouring": "ulewa", - "rainy": "deszczowo", - "snowy": "\u015bnie\u017cnie", - "snowy-rainy": "\u015bnie\u017cnie, deszczowo", - "sunny": "s\u0142onecznie", - "windy": "wietrznie", - "windy-variant": "wietrznie" + "pouring": "Ulewa", + "rainy": "Deszczowo", + "snowy": "\u015anie\u017cnie", + "snowy-rainy": "\u015anie\u017cnie, deszczowo", + "sunny": "S\u0142onecznie", + "windy": "Wietrznie", + "windy-variant": "Wietrznie" } } } \ No newline at end of file diff --git a/homeassistant/components/withings/translations/pl.json b/homeassistant/components/withings/translations/pl.json index 626e74a36ac..e6b31e4d481 100644 --- a/homeassistant/components/withings/translations/pl.json +++ b/homeassistant/components/withings/translations/pl.json @@ -21,7 +21,7 @@ }, "reauth": { "description": "Profil \"{profile}\" musi zosta\u0107 ponownie uwierzytelniony, aby nadal otrzymywa\u0107 dane Withings.", - "title": "Ponownie uwierzytelnij {profile}" + "title": "Ponownie uwierzytelnij {rofile}" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/pl.json b/homeassistant/components/xiaomi_aqara/translations/pl.json index 46a18c457af..a603566d569 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pl.json +++ b/homeassistant/components/xiaomi_aqara/translations/pl.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki Xiaomi Aqara, spr\u00f3buj u\u017cy\u0107 adresu IP urz\u0105dzenia, na kt\u00f3rym pracuje Home Assistant jako interfejsu.", - "invalid_host": "Adres IP jest nieprawid\u0142owy.", + "invalid_host": "Adres IP jest nieprawid\u0142owy, po pomoc w rozwi\u0105zaniu problemu wejd\u017a tutaj: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Nieprawid\u0142owy interfejs sieciowy.", "invalid_key": "Nieprawid\u0142owy klucz bramki.", "invalid_mac": "Nieprawid\u0142owy adres MAC.", @@ -36,7 +36,7 @@ "interface": "Interfejs sieciowy", "mac": "Adres MAC (opcjonalnie)" }, - "description": "Po\u0142\u0105cz si\u0119 z bramk\u0105 Xiaomi Aqara", + "description": "Po\u0142\u0105cz si\u0119 z bramk\u0105 Xiaomi Aqara, je\u015bli zostawisz Adres IP oraz MAC puste to bramka zostanie automatycznie wykryta.", "title": "Bramka Xiaomi Aqara" } } diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index b4bd9a5546d..90f191a58c4 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -16,7 +16,7 @@ "name": "Nazwa bramki", "token": "Token API" }, - "description": "B\u0119dziesz potrzebowa\u0107 tokenu API, odwied\u017a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token, aby uzyska\u0107 instrukcje.", + "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara .", "title": "Po\u0142\u0105cz si\u0119 z bramk\u0105 Xiaomi" }, "user": { diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json new file mode 100644 index 00000000000..7fd730d5248 --- /dev/null +++ b/homeassistant/components/yeelight/translations/pl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem" + }, + "step": { + "pick_device": { + "data": { + "device": "Urz\u0105dzenie" + } + }, + "user": { + "data": { + "host": "Host", + "ip_address": "Adres IP" + }, + "description": "Je\u015bli nie podasz hosta, to wykrywanie zostanie u\u017cyte do odnalezienia urz\u0105dze\u0144." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Model (Opcjonalne)", + "nightlight_switch": "U\u017cyj prze\u0142\u0105cznika Nocnego wiat\u0142a", + "save_on_change": "Zachowaj status po zmianie", + "transition": "Czas przej\u015bcia (ms)", + "use_music_mode": "Aktywuj tryb muzyczny" + }, + "description": "Je\u015bli nie podasz modelu urz\u0105dzenia, zostanie on automatycznie wykryty." + } + } + }, + "title": "Yeelight" +} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/pl.json b/homeassistant/components/zwave/translations/pl.json index f871ab928b6..17ab30c1898 100644 --- a/homeassistant/components/zwave/translations/pl.json +++ b/homeassistant/components/zwave/translations/pl.json @@ -21,8 +21,8 @@ "state": { "_": { "dead": "martwy", - "initializing": "inicjalizacja", - "ready": "gotowy", + "initializing": "Inicjowanie", + "ready": "Gotowe", "sleeping": "u\u015bpiony" }, "query_stage": { From 3b88452b2f642bd1d87c21ee03042cc08f0816e7 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Sun, 6 Sep 2020 04:12:36 -0400 Subject: [PATCH 674/862] Upgrade TensorFlow to 2.3 (#39673) --- homeassistant/components/tensorflow/manifest.json | 5 ++--- requirements_all.txt | 7 ++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 6b5218b64b1..2f1c391094c 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -3,9 +3,8 @@ "name": "TensorFlow", "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ - "tensorflow==2.2.0", - "tf-slim==1.1.0", - "tf-models-official==2.2.1", + "tensorflow==2.3.0", + "tf-models-official==2.3.0", "pycocotools==2.0.1", "numpy==1.19.1", "pillow==7.2.0" diff --git a/requirements_all.txt b/requirements_all.txt index 044c3fb0db7..3d28e9d5e36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2119,7 +2119,7 @@ temescal==0.3 temperusb==1.5.3 # homeassistant.components.tensorflow -# tensorflow==2.2.0 +# tensorflow==2.3.0 # homeassistant.components.powerwall tesla-powerwall==0.2.12 @@ -2128,10 +2128,7 @@ tesla-powerwall==0.2.12 teslajsonpy==0.10.4 # homeassistant.components.tensorflow -# tf-models-official==2.2.1 - -# homeassistant.components.tensorflow -tf-slim==1.1.0 +# tf-models-official==2.3.0 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 From 36632f64f1dd89dd34903db62cd705b66f53b113 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 6 Sep 2020 11:54:08 +0200 Subject: [PATCH 675/862] Mock tradfri lights correctly (#39706) --- tests/components/tradfri/test_light.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 653a9ce62df..cf11d42411e 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -135,6 +135,10 @@ def mock_light(test_features=None, test_state=None, light_number=0): reachable=True, observe=Mock(), device_info=dev_info_mock, + has_light_control=True, + has_socket_control=False, + has_blind_control=False, + has_signal_repeater_control=False, ) _mock_light.name = f"tradfri_light_{light_number}" From 405311e89f754dfe9013ddaabfaf25aebd0b772b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 6 Sep 2020 12:01:14 +0200 Subject: [PATCH 676/862] Bump pytradfri to 7.0.2 (#39707) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 334f6246508..dace0f0739b 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", "requirements": [ - "pytradfri[async]==7.0.1" + "pytradfri[async]==7.0.2" ], "homekit": { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3d28e9d5e36..5dce4e3645d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1805,7 +1805,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.1 +pytradfri[async]==7.0.2 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cba38c5477b..44aad4fd6e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -851,7 +851,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.1 +pytradfri[async]==7.0.2 # homeassistant.components.vera pyvera==0.3.9 From 13a6aaa6ff5572344759985cd5736d3c5ed2a153 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 6 Sep 2020 07:53:45 -0500 Subject: [PATCH 677/862] Add media browser support to roku (#39652) Co-authored-by: Paulus Schoutsen --- .../components/media_player/const.py | 2 + homeassistant/components/roku/media_player.py | 103 ++++++++++++- tests/components/roku/test_media_player.py | 143 ++++++++++++++++++ 3 files changed, 245 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 64d77a4889e..4c8fdace9d7 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -35,11 +35,13 @@ MEDIA_TYPE_MOVIE = "movie" MEDIA_TYPE_VIDEO = "video" MEDIA_TYPE_EPISODE = "episode" MEDIA_TYPE_CHANNEL = "channel" +MEDIA_TYPE_CHANNELS = "channels" MEDIA_TYPE_PLAYLIST = "playlist" MEDIA_TYPE_IMAGE = "image" MEDIA_TYPE_URL = "url" MEDIA_TYPE_GAME = "game" MEDIA_TYPE_APP = "app" +MEDIA_TYPE_APPS = "apps" MEDIA_TYPE_ALBUM = "album" MEDIA_TYPE_TRACK = "track" MEDIA_TYPE_ARTIST = "artist" diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 1aa4fc3e9bb..c25145ae2d7 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -11,7 +11,10 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( MEDIA_TYPE_APP, + MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_CHANNELS, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -23,6 +26,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, ) +from homeassistant.components.media_player.errors import BrowseError from homeassistant.const import ( STATE_HOME, STATE_IDLE, @@ -49,6 +53,7 @@ SUPPORT_ROKU = ( | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_ON | SUPPORT_TURN_OFF + | SUPPORT_BROWSE_MEDIA ) SEARCH_SCHEMA = {vol.Required(ATTR_KEYWORD): str} @@ -69,6 +74,41 @@ async def async_setup_entry(hass, entry, async_add_entities): ) +def browse_media_library(channels: bool = False) -> dict: + """Create response payload to describe contents of a specific library.""" + library_info = { + "title": "Media Library", + "media_content_id": "library", + "media_content_type": "library", + "can_play": False, + "can_expand": True, + "children": [], + } + + library_info["children"].append( + { + "title": "Apps", + "media_content_id": "apps", + "media_content_type": MEDIA_TYPE_APPS, + "can_expand": True, + "can_play": False, + } + ) + + if channels: + library_info["children"].append( + { + "title": "Channels", + "media_content_id": "channels", + "media_content_type": MEDIA_TYPE_CHANNELS, + "can_expand": True, + "can_play": False, + } + ) + + return library_info + + class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Representation of a Roku media player on the network.""" @@ -234,6 +274,58 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Emulate opening the search screen and entering the search keyword.""" await self.coordinator.roku.search(keyword) + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + if media_content_type in [None, "library"]: + is_tv = self.coordinator.data.info.device_type == "tv" + return browse_media_library(channels=is_tv) + + response = None + + if media_content_type == MEDIA_TYPE_APPS: + response = { + "title": "Apps", + "media_content_id": "apps", + "media_content_type": MEDIA_TYPE_APPS, + "can_expand": True, + "can_play": False, + "children": [ + { + "title": app.name, + "thumbnail": self.coordinator.roku.app_icon_url(app.app_id), + "media_content_id": app.app_id, + "media_content_type": MEDIA_TYPE_APP, + "can_play": True, + } + for app in self.coordinator.data.apps + ], + } + + if media_content_type == MEDIA_TYPE_CHANNELS: + response = { + "title": "Channels", + "media_content_id": "channels", + "media_content_type": MEDIA_TYPE_CHANNELS, + "can_expand": True, + "can_play": False, + "children": [ + { + "title": channel.name, + "media_content_id": channel.number, + "media_content_type": MEDIA_TYPE_CHANNEL, + "can_play": True, + } + for channel in self.coordinator.data.channels + ], + } + + if response is None: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) + + return response + @roku_exception_handler async def async_turn_on(self) -> None: """Turn on the Roku.""" @@ -298,15 +390,20 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): @roku_exception_handler async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Tune to channel.""" - if media_type != MEDIA_TYPE_CHANNEL: + if media_type not in (MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL): _LOGGER.error( - "Invalid media type %s. Only %s is supported", + "Invalid media type %s. Only %s and %s are supported", media_type, + MEDIA_TYPE_APP, MEDIA_TYPE_CHANNEL, ) return - await self.coordinator.roku.tune(media_id) + if media_type == MEDIA_TYPE_APP: + await self.coordinator.roku.launch(media_id) + elif media_type == MEDIA_TYPE_CHANNEL: + await self.coordinator.roku.tune(media_id) + await self.coordinator.async_request_refresh() @roku_exception_handler diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index b355e6178ff..312770a873a 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -17,9 +17,12 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MP_DOMAIN, MEDIA_TYPE_APP, + MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_CHANNELS, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -32,6 +35,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.components.roku.const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH +from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_MEDIA_NEXT_TRACK, @@ -158,6 +162,7 @@ async def test_supported_features( | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_ON | SUPPORT_TURN_OFF + | SUPPORT_BROWSE_MEDIA == state.attributes.get("supported_features") ) @@ -187,6 +192,7 @@ async def test_tv_supported_features( | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_ON | SUPPORT_TURN_OFF + | SUPPORT_BROWSE_MEDIA == state.attributes.get("supported_features") ) @@ -364,6 +370,20 @@ async def test_services( remote_mock.assert_called_once_with("reverse") + with patch("homeassistant.components.roku.Roku.launch") as launch_mock: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: MAIN_ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_APP, + ATTR_MEDIA_CONTENT_ID: "11", + }, + blocking=True, + ) + + launch_mock.assert_called_once_with("11") + with patch("homeassistant.components.roku.Roku.remote") as remote_mock: await hass.services.async_call( MP_DOMAIN, @@ -450,6 +470,129 @@ async def test_tv_services( tune_mock.assert_called_once_with("55") +async def test_media_browse(hass, aioclient_mock, hass_ws_client): + """Test browsing media.""" + await setup_integration( + hass, + aioclient_mock, + device="rokutv", + app="tvinput-dtv", + host=TV_HOST, + unique_id=TV_SERIAL, + ) + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": TV_ENTITY_ID, + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + assert msg["result"] + assert msg["result"]["title"] == "Media Library" + assert msg["result"]["media_content_type"] == "library" + assert msg["result"]["can_expand"] + assert not msg["result"]["can_play"] + assert len(msg["result"]["children"]) == 2 + + # test apps + await client.send_json( + { + "id": 2, + "type": "media_player/browse_media", + "entity_id": TV_ENTITY_ID, + "media_content_type": MEDIA_TYPE_APPS, + "media_content_id": "apps", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 2 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + assert msg["result"] + assert msg["result"]["title"] == "Apps" + assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS + assert msg["result"]["can_expand"] + assert not msg["result"]["can_play"] + assert len(msg["result"]["children"]) == 11 + + assert msg["result"]["children"][0]["title"] == "Satellite TV" + assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP + assert msg["result"]["children"][0]["media_content_id"] == "tvinput.hdmi2" + assert ( + msg["result"]["children"][0]["thumbnail"] + == "http://192.168.1.161:8060/query/icon/tvinput.hdmi2" + ) + assert msg["result"]["children"][0]["can_play"] + + assert msg["result"]["children"][3]["title"] == "Roku Channel Store" + assert msg["result"]["children"][3]["media_content_type"] == MEDIA_TYPE_APP + assert msg["result"]["children"][3]["media_content_id"] == "11" + assert ( + msg["result"]["children"][3]["thumbnail"] + == "http://192.168.1.161:8060/query/icon/11" + ) + assert msg["result"]["children"][3]["can_play"] + + # test channels + await client.send_json( + { + "id": 3, + "type": "media_player/browse_media", + "entity_id": TV_ENTITY_ID, + "media_content_type": MEDIA_TYPE_CHANNELS, + "media_content_id": "channels", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 3 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + assert msg["result"] + assert msg["result"]["title"] == "Channels" + assert msg["result"]["media_content_type"] == MEDIA_TYPE_CHANNELS + assert msg["result"]["can_expand"] + assert not msg["result"]["can_play"] + assert len(msg["result"]["children"]) == 2 + + assert msg["result"]["children"][0]["title"] == "WhatsOn" + assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_CHANNEL + assert msg["result"]["children"][0]["media_content_id"] == "1.1" + assert msg["result"]["children"][0]["can_play"] + + # test invalid media type + await client.send_json( + { + "id": 4, + "type": "media_player/browse_media", + "entity_id": TV_ENTITY_ID, + "media_content_type": "invalid", + "media_content_id": "invalid", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 4 + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + + async def test_integration_services( hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: From df8daf561eb8e9799bd69836f5cf116de1ef038c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 Sep 2020 15:52:59 +0200 Subject: [PATCH 678/862] Browse media class (#39698) --- .../components/arcam_fmj/media_player.py | 30 ++--- .../components/media_player/__init__.py | 63 +++++++++- .../components/media_player/const.py | 30 ++--- .../components/media_source/__init__.py | 4 +- .../components/media_source/local_source.py | 46 ++++---- .../components/media_source/models.py | 75 ++++++------ .../components/netatmo/media_source.py | 29 ++--- .../components/philips_js/media_player.py | 35 +++--- .../components/plex/media_browser.py | 13 +- homeassistant/components/roku/media_player.py | 111 +++++++++--------- .../components/sonos/media_player.py | 20 ++-- .../components/spotify/media_player.py | 57 ++++----- tests/components/media_source/test_init.py | 19 ++- tests/components/media_source/test_models.py | 45 ++++++- 14 files changed, 342 insertions(+), 235 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 17607cd3f8c..7e6c34a8324 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -5,7 +5,7 @@ from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceC from arcam.fmj.state import State from homeassistant import config_entries -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_BROWSE_MEDIA, @@ -253,22 +253,24 @@ class ArcamFmj(MediaPlayerEntity): presets = self._state.get_preset_details() radio = [ - { - "title": preset.name, - "media_content_id": f"preset:{preset.index}", - "media_content_type": MEDIA_TYPE_MUSIC, - "can_play": True, - } + BrowseMedia( + title=preset.name, + media_content_id=f"preset:{preset.index}", + media_content_type=MEDIA_TYPE_MUSIC, + can_play=True, + can_expand=False, + ) for preset in presets.values() ] - root = { - "title": "Root", - "media_content_id": "root", - "media_content_type": "library", - "can_play": False, - "children": radio, - } + root = BrowseMedia( + title="Root", + media_content_id="root", + media_content_type="library", + can_play=False, + can_expand=True, + children=radio, + ) return root diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 886f0170a06..16cabe5edb9 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -7,7 +7,7 @@ import functools as ft import hashlib import logging from random import SystemRandom -from typing import Optional +from typing import List, Optional from urllib.parse import urlparse from aiohttp import web @@ -811,7 +811,11 @@ class MediaPlayerEntity(Entity): return state_attr - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, + media_content_type: Optional[str] = None, + media_content_id: Optional[str] = None, + ) -> "BrowseMedia": """ Return a payload for the "media_player/browse_media" websocket command. @@ -976,7 +980,7 @@ async def websocket_browse_media(hass, connection, msg): To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() """ component = hass.data[DOMAIN] - player = component.get_entity(msg["entity_id"]) + player: Optional[MediaPlayerDevice] = component.get_entity(msg["entity_id"]) if player is None: connection.send_error(msg["id"], "entity_not_found", "Entity not found") @@ -1015,6 +1019,12 @@ async def websocket_browse_media(hass, connection, msg): ) return + # For backwards compat + if isinstance(payload, BrowseMedia): + payload = payload.as_dict() + else: + _LOGGER.warning("Browse Media should use new BrowseMedia class") + connection.send_result(msg["id"], payload) @@ -1028,3 +1038,50 @@ class MediaPlayerDevice(MediaPlayerEntity): "MediaPlayerDevice is deprecated, modify %s to extend MediaPlayerEntity", cls.__name__, ) + + +class BrowseMedia: + """Represent a browsable media file.""" + + def __init__( + self, + *, + media_content_id: str, + media_content_type: str, + title: str, + can_play: bool, + can_expand: bool, + children: Optional[List["BrowseMedia"]] = None, + thumbnail: Optional[str] = None, + ): + """Initialize browse media item.""" + self.media_content_id = media_content_id + self.media_content_type = media_content_type + self.title = title + self.can_play = can_play + self.can_expand = can_expand + self.children = children + self.thumbnail = thumbnail + + def as_dict(self, *, parent: bool = True) -> dict: + """Convert Media class to browse media dictionary.""" + response = { + "title": self.title, + "media_content_type": self.media_content_type, + "media_content_id": self.media_content_id, + "can_play": self.can_play, + "can_expand": self.can_expand, + "thumbnail": self.thumbnail, + } + + if not parent: + return response + + if self.children: + response["children"] = [ + child.as_dict(parent=False) for child in self.children + ] + else: + response["children"] = [] + + return response diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 4c8fdace9d7..74c65fcd780 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -29,27 +29,27 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list" DOMAIN = "media_player" -MEDIA_TYPE_MUSIC = "music" -MEDIA_TYPE_TVSHOW = "tvshow" -MEDIA_TYPE_MOVIE = "movie" -MEDIA_TYPE_VIDEO = "video" -MEDIA_TYPE_EPISODE = "episode" -MEDIA_TYPE_CHANNEL = "channel" -MEDIA_TYPE_CHANNELS = "channels" -MEDIA_TYPE_PLAYLIST = "playlist" -MEDIA_TYPE_IMAGE = "image" -MEDIA_TYPE_URL = "url" -MEDIA_TYPE_GAME = "game" +MEDIA_TYPE_ALBUM = "album" MEDIA_TYPE_APP = "app" MEDIA_TYPE_APPS = "apps" -MEDIA_TYPE_ALBUM = "album" -MEDIA_TYPE_TRACK = "track" MEDIA_TYPE_ARTIST = "artist" +MEDIA_TYPE_CHANNEL = "channel" +MEDIA_TYPE_CHANNELS = "channels" +MEDIA_TYPE_COMPOSER = "composer" MEDIA_TYPE_CONTRIBUTING_ARTIST = "contributing_artist" +MEDIA_TYPE_EPISODE = "episode" +MEDIA_TYPE_GAME = "game" +MEDIA_TYPE_GENRE = "genre" +MEDIA_TYPE_IMAGE = "image" +MEDIA_TYPE_MOVIE = "movie" +MEDIA_TYPE_MUSIC = "music" +MEDIA_TYPE_PLAYLIST = "playlist" MEDIA_TYPE_PODCAST = "podcast" MEDIA_TYPE_SEASON = "season" -MEDIA_TYPE_GENRE = "genre" -MEDIA_TYPE_COMPOSER = "composer" +MEDIA_TYPE_TRACK = "track" +MEDIA_TYPE_TVSHOW = "tvshow" +MEDIA_TYPE_URL = "url" +MEDIA_TYPE_VIDEO = "video" SERVICE_CLEAR_PLAYLIST = "clear_playlist" SERVICE_PLAY_MEDIA = "play_media" diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 6dc4eecc6dc..22f633d21d0 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -68,7 +68,7 @@ def _get_media_item( @bind_hass async def async_browse_media( hass: HomeAssistant, media_content_id: str -) -> models.BrowseMedia: +) -> models.BrowseMediaSource: """Return media player browse media results.""" return await _get_media_item(hass, media_content_id).async_browse() @@ -94,7 +94,7 @@ async def websocket_browse_media(hass, connection, msg): media = await async_browse_media(hass, msg.get("media_content_id")) connection.send_result( msg["id"], - media.to_media_player_item(), + media.as_dict(), ) except BrowseError as err: connection.send_error(msg["id"], "browse_media_failed", str(err)) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 2374edf0c33..34b526171ea 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.util import sanitize_path from .const import DOMAIN, MEDIA_MIME_TYPES -from .models import BrowseMedia, MediaSource, MediaSourceItem, PlayMedia +from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia @callback @@ -67,7 +67,7 @@ class LocalSource(MediaSource): async def async_browse_media( self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES - ) -> BrowseMedia: + ) -> BrowseMediaSource: """Return media.""" try: source_dir_id, location = async_parse_identifier(item) @@ -92,37 +92,41 @@ class LocalSource(MediaSource): def _build_item_response(self, source_dir_id: str, path: Path, is_child=False): mime_type, _ = mimetypes.guess_type(str(path)) - media = BrowseMedia( - DOMAIN, - f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", - path.name, - path.is_file(), - path.is_dir(), - mime_type, - ) + is_file = path.is_file() + is_dir = path.is_dir() # Make sure it's a file or directory - if not media.can_play and not media.can_expand: + if not is_file and not is_dir: return None # Check that it's a media file - if media.can_play and ( + if is_file and ( not mime_type or mime_type.split("/")[0] not in MEDIA_MIME_TYPES ): return None - if not media.can_expand: + title = path.name + if is_dir: + title += "/" + + media = BrowseMediaSource( + domain=DOMAIN, + identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", + media_content_type="directory", + title=title, + can_play=is_file, + can_expand=is_dir, + ) + + if is_file or is_child: return media - media.name += "/" - # Append first level children - if not is_child: - media.children = [] - for child_path in path.iterdir(): - child = self._build_item_response(source_dir_id, child_path, True) - if child: - media.children.append(child) + media.children = [] + for child_path in path.iterdir(): + child = self._build_item_response(source_dir_id, child_path, True) + if child: + media.children.append(child) return media diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index b93cb961449..cd8e44f4a24 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -3,6 +3,11 @@ from abc import ABC from dataclasses import dataclass from typing import List, Optional, Tuple +from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_CHANNELS, +) from homeassistant.core import HomeAssistant, callback from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX @@ -16,49 +21,21 @@ class PlayMedia: mime_type: str -@dataclass -class BrowseMedia: +class BrowseMediaSource(BrowseMedia): """Represent a browsable media file.""" - domain: str - identifier: str + children: Optional[List["BrowseMediaSource"]] - name: str - can_play: bool = False - can_expand: bool = False - media_content_type: str = None - children: List = None - thumbnail: str = None + def __init__(self, *, domain: Optional[str], identifier: Optional[str], **kwargs): + """Initialize media source browse media.""" + media_content_id = f"{URI_SCHEME}{domain or ''}" + if identifier: + media_content_id += f"/{identifier}" - def to_uri(self): - """Return URI of media.""" - uri = f"{URI_SCHEME}{self.domain or ''}" - if self.identifier: - uri += f"/{self.identifier}" - return uri + super().__init__(media_content_id=media_content_id, **kwargs) - def to_media_player_item(self): - """Convert Media class to browse media dictionary.""" - content_type = self.media_content_type - - if content_type is None: - content_type = "folder" if self.can_expand else "file" - - response = { - "title": self.name, - "media_content_type": content_type, - "media_content_id": self.to_uri(), - "can_play": self.can_play, - "can_expand": self.can_expand, - "thumbnail": self.thumbnail, - } - - if self.children: - response["children"] = [ - child.to_media_player_item() for child in self.children - ] - - return response + self.domain = domain + self.identifier = identifier @dataclass @@ -69,12 +46,26 @@ class MediaSourceItem: domain: Optional[str] identifier: str - async def async_browse(self) -> BrowseMedia: + async def async_browse(self) -> BrowseMediaSource: """Browse this item.""" if self.domain is None: - base = BrowseMedia(None, None, "Media Sources", False, True) + base = BrowseMediaSource( + domain=None, + identifier=None, + media_content_type=MEDIA_TYPE_CHANNELS, + title="Media Sources", + can_play=False, + can_expand=True, + ) base.children = [ - BrowseMedia(source.domain, None, source.name, False, True) + BrowseMediaSource( + domain=source.domain, + identifier=None, + media_content_type=MEDIA_TYPE_CHANNEL, + title=source.name, + can_play=False, + can_expand=True, + ) for source in self.hass.data[DOMAIN].values() ] return base @@ -121,6 +112,6 @@ class MediaSource(ABC): async def async_browse_media( self, item: MediaSourceItem, media_types: Tuple[str] - ) -> BrowseMedia: + ) -> BrowseMediaSource: """Browse media.""" raise NotImplementedError diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index a862252f02e..d5dcb2e6059 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -3,11 +3,12 @@ import datetime as dt import re from typing import Optional, Tuple +from homeassistant.components.media_player.const import MEDIA_TYPE_VIDEO from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.const import MEDIA_MIME_TYPES from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( - BrowseMedia, + BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia, @@ -43,7 +44,7 @@ class NetatmoSource(MediaSource): async def async_browse_media( self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES - ) -> Optional[BrowseMedia]: + ) -> Optional[BrowseMediaSource]: """Return media.""" try: source, camera_id, event_id = async_parse_identifier(item) @@ -54,7 +55,7 @@ class NetatmoSource(MediaSource): def _browse_media( self, source: str, camera_id: str, event_id: int - ) -> Optional[BrowseMedia]: + ) -> Optional[BrowseMediaSource]: """Browse media.""" if camera_id and camera_id not in self.events: raise BrowseError("Camera does not exist.") @@ -66,7 +67,7 @@ class NetatmoSource(MediaSource): def _build_item_response( self, source: str, camera_id: str, event_id: int = None - ) -> Optional[BrowseMedia]: + ) -> Optional[BrowseMediaSource]: if event_id and event_id in self.events[camera_id]: created = dt.datetime.fromtimestamp(event_id) thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url") @@ -81,18 +82,18 @@ class NetatmoSource(MediaSource): else: path = f"{source}/{camera_id}" - media = BrowseMedia( - DOMAIN, - path, - title, + media = BrowseMediaSource( + domain=DOMAIN, + identifier=path, + media_content_type=MEDIA_TYPE_VIDEO, + title=title, + can_play=bool( + event_id and self.events[camera_id][event_id].get("media_url") + ), + can_expand=event_id is None, + thumbnail=thumbnail, ) - media.can_play = bool( - event_id and self.events[camera_id][event_id].get("media_url") - ) - media.can_expand = event_id is None - media.thumbnail = thumbnail - if not media.can_play and not media.can_expand: return None diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 9137a61d835..0e2d3f97c49 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -5,9 +5,14 @@ import logging from haphilipsjs import PhilipsTV import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, + BrowseMedia, + MediaPlayerEntity, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, @@ -281,21 +286,23 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): f"Media not found: {media_content_type} / {media_content_id}" ) - return { - "title": "Channels", - "media_content_id": "", - "media_content_type": "library", - "can_play": False, - "children": [ - { - "title": channel, - "media_content_id": channel, - "media_content_type": MEDIA_TYPE_CHANNEL, - "can_play": True, - } + return BrowseMedia( + title="Channels", + media_content_id="", + media_content_type=MEDIA_TYPE_CHANNELS, + can_play=False, + can_expand=True, + children=[ + BrowseMedia( + title=channel, + media_content_id=channel, + media_content_type=MEDIA_TYPE_CHANNEL, + can_play=True, + can_expand=False, + ) for channel in self._channels.values() ], - } + ) def update(self): """Get the latest data and update device state.""" diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index e8d5d32c66e..39ad44f5ff1 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -1,6 +1,7 @@ """Support to interface with the Plex API.""" import logging +from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.errors import BrowseError from .const import DOMAIN @@ -34,10 +35,10 @@ def browse_media( return None media_info = item_payload(media) - if media_info.get("can_expand"): - media_info["children"] = [] + if media_info.can_expand: + media_info.children = [] for item in media: - media_info["children"].append(item_payload(item)) + media_info.children.append(item_payload(item)) return media_info if media_content_id and ":" in media_content_id: @@ -103,12 +104,12 @@ def item_payload(item): "media_content_id": str(item.ratingKey), "media_content_type": item.type, "can_play": True, + "can_expand": item.type in EXPANDABLES, } if hasattr(item, "thumbUrl"): payload["thumbnail"] = item.thumbUrl - if item.type in EXPANDABLES: - payload["can_expand"] = True - return payload + + return BrowseMedia(**payload) def library_section_payload(section): diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index c25145ae2d7..c8f1b6d999d 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( DEVICE_CLASS_RECEIVER, DEVICE_CLASS_TV, + BrowseMedia, MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( @@ -74,36 +75,36 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -def browse_media_library(channels: bool = False) -> dict: +def browse_media_library(channels: bool = False) -> BrowseMedia: """Create response payload to describe contents of a specific library.""" - library_info = { - "title": "Media Library", - "media_content_id": "library", - "media_content_type": "library", - "can_play": False, - "can_expand": True, - "children": [], - } + library_info = BrowseMedia( + title="Media Library", + media_content_id="library", + media_content_type="library", + can_play=False, + can_expand=True, + children=[], + ) - library_info["children"].append( - { - "title": "Apps", - "media_content_id": "apps", - "media_content_type": MEDIA_TYPE_APPS, - "can_expand": True, - "can_play": False, - } + library_info.children.append( + BrowseMedia( + title="Apps", + media_content_id="apps", + media_content_type=MEDIA_TYPE_APPS, + can_expand=True, + can_play=False, + ) ) if channels: - library_info["children"].append( - { - "title": "Channels", - "media_content_id": "channels", - "media_content_type": MEDIA_TYPE_CHANNELS, - "can_expand": True, - "can_play": False, - } + library_info.children.append( + BrowseMedia( + title="Channels", + media_content_id="channels", + media_content_type=MEDIA_TYPE_CHANNELS, + can_expand=True, + can_play=False, + ) ) return library_info @@ -283,41 +284,43 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): response = None if media_content_type == MEDIA_TYPE_APPS: - response = { - "title": "Apps", - "media_content_id": "apps", - "media_content_type": MEDIA_TYPE_APPS, - "can_expand": True, - "can_play": False, - "children": [ - { - "title": app.name, - "thumbnail": self.coordinator.roku.app_icon_url(app.app_id), - "media_content_id": app.app_id, - "media_content_type": MEDIA_TYPE_APP, - "can_play": True, - } + response = BrowseMedia( + title="Apps", + media_content_id="apps", + media_content_type=MEDIA_TYPE_APPS, + can_expand=True, + can_play=False, + children=[ + BrowseMedia( + title=app.name, + thumbnail=self.coordinator.roku.app_icon_url(app.app_id), + media_content_id=app.app_id, + media_content_type=MEDIA_TYPE_APP, + can_play=True, + can_expand=False, + ) for app in self.coordinator.data.apps ], - } + ) if media_content_type == MEDIA_TYPE_CHANNELS: - response = { - "title": "Channels", - "media_content_id": "channels", - "media_content_type": MEDIA_TYPE_CHANNELS, - "can_expand": True, - "can_play": False, - "children": [ - { - "title": channel.name, - "media_content_id": channel.number, - "media_content_type": MEDIA_TYPE_CHANNEL, - "can_play": True, - } + response = BrowseMedia( + title="Channels", + media_content_id="channels", + media_content_type=MEDIA_TYPE_CHANNELS, + can_expand=True, + can_play=False, + children=[ + BrowseMedia( + title=channel.name, + media_content_id=channel.number, + media_content_type=MEDIA_TYPE_CHANNEL, + can_play=True, + can_expand=False, + ) for channel in self.coordinator.data.channels ], - } + ) if response is None: raise BrowseError( diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 3ad22de9151..3da46b07e9e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -14,7 +14,7 @@ import pysonos.music_library import pysonos.snapshot import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_ALBUM, @@ -1462,15 +1462,15 @@ def build_item_response(media_library, payload): except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] - return { - "title": title, - "thumbnail": thumbnail, - "media_content_id": payload["idstring"], - "media_content_type": payload["search_type"], - "children": [item_payload(item) for item in media], - "can_play": can_play(payload["search_type"]), - "can_expand": can_expand(payload["search_type"]), - } + return BrowseMedia( + title=title, + thumbnail=thumbnail, + media_content_id=payload["idstring"], + media_content_type=payload["search_type"], + children=[item_payload(item) for item in media], + can_play=can_play(payload["search_type"]), + can_expand=can_expand(payload["search_type"]), + ) def item_payload(item): diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 1b3140327ed..7f7659061e8 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -9,7 +9,7 @@ from aiohttp import ClientError from spotipy import Spotify, SpotifyException from yarl import URL -from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -439,28 +439,30 @@ def build_item_response(spotify, payload): items = media.get("items", []) else: media = None + items = [] if media is None: return None + if title is None: + if "name" in media: + title = media.get("name") + else: + title = LIBRARY_MAP.get(payload["media_content_id"]) + response = { + "title": title, "media_content_id": payload.get("media_content_id"), "media_content_type": payload.get("media_content_type"), "can_play": payload.get("media_content_type") in PLAYABLE_MEDIA_TYPES, "children": [item_payload(item) for item in items], + "can_expand": True, } - if "name" in media: - response["title"] = media.get("name") - elif title: - response["title"] = title - else: - response["title"] = LIBRARY_MAP.get(payload["media_content_id"]) - if "images" in media: response["thumbnail"] = fetch_image_url(media) - return response + return BrowseMedia(**response) def item_payload(item): @@ -469,32 +471,31 @@ def item_payload(item): Used by async_browse_media. """ + can_expand = item.get("type") not in [None, MEDIA_TYPE_TRACK] + if ( MEDIA_TYPE_TRACK in item or item.get("type") != MEDIA_TYPE_ALBUM and "playlists" in item ): track = item.get(MEDIA_TYPE_TRACK) - payload = { - "title": track.get("name"), - "thumbnail": fetch_image_url(track.get(MEDIA_TYPE_ALBUM, {})), - "media_content_id": track.get("uri"), - "media_content_type": MEDIA_TYPE_TRACK, - "can_play": True, - } - else: - payload = { - "title": item.get("name"), - "thumbnail": fetch_image_url(item), - "media_content_id": item.get("uri"), - "media_content_type": item.get("type"), - "can_play": item.get("type") in PLAYABLE_MEDIA_TYPES, - } + return BrowseMedia( + title=track.get("name"), + thumbnail=fetch_image_url(track.get(MEDIA_TYPE_ALBUM, {})), + media_content_id=track.get("uri"), + media_content_type=MEDIA_TYPE_TRACK, + can_play=True, + can_expand=can_expand, + ) - if item.get("type") not in [None, MEDIA_TYPE_TRACK]: - payload["can_expand"] = True - - return payload + return BrowseMedia( + title=item.get("name"), + thumbnail=fetch_image_url(item), + media_content_id=item.get("uri"), + media_content_type=item.get("type"), + can_play=item.get("type") in PLAYABLE_MEDIA_TYPES, + can_expand=can_expand, + ) def library_payload(): diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index bc1d901e03f..eb387fcc6a3 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -41,8 +41,8 @@ async def test_async_browse_media(hass): # Test non-media ignored (/media has test.mp3 and not_media.txt) media = await media_source.async_browse_media(hass, "") - assert isinstance(media, media_source.models.BrowseMedia) - assert media.name == "media/" + assert isinstance(media, media_source.models.BrowseMediaSource) + assert media.title == "media/" assert len(media.children) == 1 # Test invalid media content @@ -51,9 +51,9 @@ async def test_async_browse_media(hass): # Test base URI returns all domains media = await media_source.async_browse_media(hass, const.URI_SCHEME) - assert isinstance(media, media_source.models.BrowseMedia) + assert isinstance(media, media_source.models.BrowseMediaSource) assert len(media.children) == 1 - assert media.children[0].name == "Local Media" + assert media.children[0].title == "Local Media" async def test_async_resolve_media(hass): @@ -73,7 +73,14 @@ async def test_websocket_browse_media(hass, hass_ws_client): client = await hass_ws_client(hass) - media = media_source.models.BrowseMedia(const.DOMAIN, "/media", False, True) + media = media_source.models.BrowseMediaSource( + domain=const.DOMAIN, + identifier="/media", + title="Local Media", + media_content_type="listing", + can_play=False, + can_expand=True, + ) with patch( "homeassistant.components.media_source.async_browse_media", @@ -90,7 +97,7 @@ async def test_websocket_browse_media(hass, hass_ws_client): assert msg["success"] assert msg["id"] == 1 - assert media.to_media_player_item() == msg["result"] + assert media.as_dict() == msg["result"] with patch( "homeassistant.components.media_source.async_browse_media", diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py index e7bac5acc9a..f951fcfb0c0 100644 --- a/tests/components/media_source/test_models.py +++ b/tests/components/media_source/test_models.py @@ -1,17 +1,30 @@ """Test Media Source model methods.""" +from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.components.media_source import const, models -async def test_browse_media_to_media_player_item(): - """Test BrowseMedia conversion to media player item dict.""" - base = models.BrowseMedia(const.DOMAIN, "media", "media/", False, True) +async def test_browse_media_as_dict(): + """Test BrowseMediaSource conversion to media player item dict.""" + base = models.BrowseMediaSource( + domain=const.DOMAIN, + identifier="media", + media_content_type="folder", + title="media/", + can_play=False, + can_expand=True, + ) base.children = [ - models.BrowseMedia( - const.DOMAIN, "media/test.mp3", "test.mp3", True, False, "audio/mp3" + models.BrowseMediaSource( + domain=const.DOMAIN, + identifier="media/test.mp3", + media_content_type=MEDIA_TYPE_MUSIC, + title="test.mp3", + can_play=True, + can_expand=False, ) ] - item = base.to_media_player_item() + item = base.as_dict() assert item["title"] == "media/" assert item["media_content_type"] == "folder" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" @@ -21,6 +34,26 @@ async def test_browse_media_to_media_player_item(): assert item["children"][0]["title"] == "test.mp3" +async def test_browse_media_parent_no_children(): + """Test BrowseMediaSource conversion to media player item dict.""" + base = models.BrowseMediaSource( + domain=const.DOMAIN, + identifier="media", + media_content_type="folder", + title="media/", + can_play=False, + can_expand=True, + ) + + item = base.as_dict() + assert item["title"] == "media/" + assert item["media_content_type"] == "folder" + assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" + assert not item["can_play"] + assert item["can_expand"] + assert len(item["children"]) == 0 + + async def test_media_source_default_name(): """Test MediaSource uses domain as default name.""" source = models.MediaSource(const.DOMAIN) From f298281ec468ab7e57385ebbf3c9ac292c3b9700 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 6 Sep 2020 16:05:10 +0200 Subject: [PATCH 679/862] Make multi-switches device a single device with 2 switches (#39689) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/switch.py | 24 ++--------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 9bd14c49ab3..7dd57467045 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -20,22 +20,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not relay_blocks: return - multiple_blocks = len(relay_blocks) > 1 - async_add_entities( - RelaySwitch(wrapper, block, multiple_blocks=multiple_blocks) - for block in relay_blocks - ) + async_add_entities(RelaySwitch(wrapper, block) for block in relay_blocks) class RelaySwitch(ShellyBlockEntity, SwitchEntity): """Switch that controls a relay block on Shelly devices.""" - def __init__( - self, wrapper: ShellyDeviceWrapper, block: RelayBlock, multiple_blocks - ) -> None: + def __init__(self, wrapper: ShellyDeviceWrapper, block: RelayBlock) -> None: """Initialize relay switch.""" super().__init__(wrapper, block) - self.multiple_blocks = multiple_blocks self.control_result = None @property @@ -46,19 +39,6 @@ class RelaySwitch(ShellyBlockEntity, SwitchEntity): return self.block.output - @property - def device_info(self): - """Device info.""" - if not self.multiple_blocks: - return super().device_info - - # If a device has multiple relays, we want to expose as separate device - return { - "name": self.name, - "identifiers": {(DOMAIN, self.wrapper.mac, self.block.index)}, - "via_device": (DOMAIN, self.wrapper.mac), - } - async def async_turn_on(self, **kwargs): """Turn on relay.""" self.control_result = await self.block.set_state(turn="on") From da9b077c118a4ee1e7783747b96c1312617393e9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 6 Sep 2020 16:06:09 +0200 Subject: [PATCH 680/862] Time condition can also accept an input_datetime Entity ID (#39676) --- homeassistant/helpers/condition.py | 27 +++++- homeassistant/helpers/config_validation.py | 4 +- tests/helpers/test_condition.py | 106 +++++++++++++++++++-- 3 files changed, 124 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index b2385d827a1..981e0988639 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -437,8 +437,9 @@ def async_template_from_config( def time( - before: Optional[dt_util.dt.time] = None, - after: Optional[dt_util.dt.time] = None, + hass: HomeAssistant, + before: Optional[Union[dt_util.dt.time, str]] = None, + after: Optional[Union[dt_util.dt.time, str]] = None, weekday: Union[None, str, Container[str]] = None, ) -> bool: """Test if local time condition matches. @@ -453,8 +454,28 @@ def time( if after is None: after = dt_util.dt.time(0) + elif isinstance(after, str): + after_entity = hass.states.get(after) + if not after_entity: + return False + after = dt_util.dt.time( + after_entity.attributes.get("hour", 23), + after_entity.attributes.get("minute", 59), + after_entity.attributes.get("second", 59), + ) + if before is None: before = dt_util.dt.time(23, 59, 59, 999999) + elif isinstance(before, str): + before_entity = hass.states.get(before) + if not before_entity: + return False + before = dt_util.dt.time( + before_entity.attributes.get("hour", 23), + before_entity.attributes.get("minute", 59), + before_entity.attributes.get("second", 59), + 999999, + ) if after < before: if not after <= now_time < before: @@ -488,7 +509,7 @@ def time_from_config( def time_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Validate time based if-condition.""" - return time(before, after, weekday) + return time(hass, before, after, weekday) return time_if diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 394500b5170..dd5a8b6522c 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -956,8 +956,8 @@ TIME_CONDITION_SCHEMA = vol.All( vol.Schema( { vol.Required(CONF_CONDITION): "time", - "before": time, - "after": time, + "before": vol.Any(time, vol.All(str, entity_domain("input_datetime"))), + "after": vol.Any(time, vol.All(str, entity_domain("input_datetime"))), "weekday": weekdays, } ), diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 4b02faec573..afe1c294290 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -5,6 +5,7 @@ import pytest from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition +from homeassistant.setup import async_setup_component from homeassistant.util import dt from tests.async_mock import patch @@ -226,29 +227,118 @@ async def test_time_window(hass): "homeassistant.helpers.condition.dt_util.now", return_value=dt.now().replace(hour=3), ): - assert not condition.time(after=sixam, before=sixpm) - assert condition.time(after=sixpm, before=sixam) + assert not condition.time(hass, after=sixam, before=sixpm) + assert condition.time(hass, after=sixpm, before=sixam) with patch( "homeassistant.helpers.condition.dt_util.now", return_value=dt.now().replace(hour=9), ): - assert condition.time(after=sixam, before=sixpm) - assert not condition.time(after=sixpm, before=sixam) + assert condition.time(hass, after=sixam, before=sixpm) + assert not condition.time(hass, after=sixpm, before=sixam) with patch( "homeassistant.helpers.condition.dt_util.now", return_value=dt.now().replace(hour=15), ): - assert condition.time(after=sixam, before=sixpm) - assert not condition.time(after=sixpm, before=sixam) + assert condition.time(hass, after=sixam, before=sixpm) + assert not condition.time(hass, after=sixpm, before=sixam) with patch( "homeassistant.helpers.condition.dt_util.now", return_value=dt.now().replace(hour=21), ): - assert not condition.time(after=sixam, before=sixpm) - assert condition.time(after=sixpm, before=sixam) + assert not condition.time(hass, after=sixam, before=sixpm) + assert condition.time(hass, after=sixpm, before=sixam) + + +async def test_time_using_input_datetime(hass): + """Test time conditions using input_datetime entities.""" + await async_setup_component( + hass, + "input_datetime", + { + "input_datetime": { + "am": {"has_date": True, "has_time": True}, + "pm": {"has_date": True, "has_time": True}, + } + }, + ) + + await hass.services.async_call( + "input_datetime", + "set_datetime", + { + "entity_id": "input_datetime.am", + "datetime": str( + dt.now() + .replace(hour=6, minute=0, second=0, microsecond=0) + .replace(tzinfo=None) + ), + }, + blocking=True, + ) + + await hass.services.async_call( + "input_datetime", + "set_datetime", + { + "entity_id": "input_datetime.pm", + "datetime": str( + dt.now() + .replace(hour=18, minute=0, second=0, microsecond=0) + .replace(tzinfo=None) + ), + }, + blocking=True, + ) + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt.now().replace(hour=3), + ): + assert not condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt.now().replace(hour=9), + ): + assert condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert not condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt.now().replace(hour=15), + ): + assert condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert not condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt.now().replace(hour=21), + ): + assert not condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + + assert not condition.time(hass, after="input_datetime.not_existing") + assert not condition.time(hass, before="input_datetime.not_existing") async def test_if_numeric_state_not_raise_on_unavailable(hass): From a3c45a6f8926fac6e4dd6417a92c26da34b84594 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 6 Sep 2020 16:55:06 +0200 Subject: [PATCH 681/862] Add shorthand notation for Template conditions (#39705) --- homeassistant/helpers/condition.py | 30 ++++++++++++-------- homeassistant/helpers/config_validation.py | 33 +++++++++++++--------- homeassistant/helpers/script.py | 5 +++- tests/components/automation/test_init.py | 25 ++++++++++++++++ tests/helpers/test_condition.py | 5 +--- tests/helpers/test_script.py | 32 ++++++++++++--------- 6 files changed, 87 insertions(+), 43 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 981e0988639..82839d1e0f8 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -52,26 +52,30 @@ ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool] async def async_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True + hass: HomeAssistant, + config: Union[ConfigType, Template], + config_validation: bool = True, ) -> ConditionCheckerType: """Turn a condition configuration into a method. Should be run on the event loop. """ + if isinstance(config, Template): + # We got a condition template, wrap it in a configuration to pass along. + config = { + CONF_CONDITION: "template", + CONF_VALUE_TEMPLATE: config, + } + + condition = config.get(CONF_CONDITION) for fmt in (ASYNC_FROM_CONFIG_FORMAT, FROM_CONFIG_FORMAT): - factory = getattr( - sys.modules[__name__], fmt.format(config.get(CONF_CONDITION)), None - ) + factory = getattr(sys.modules[__name__], fmt.format(condition), None) if factory: break if factory is None: - raise HomeAssistantError( - 'Invalid condition "{}" specified {}'.format( - config.get(CONF_CONDITION), config - ) - ) + raise HomeAssistantError(f'Invalid condition "{condition}" specified {config}') # Check for partials to properly determine if coroutine function check_factory = factory @@ -584,9 +588,12 @@ async def async_device_from_config( async def async_validate_condition_config( - hass: HomeAssistant, config: ConfigType -) -> ConfigType: + hass: HomeAssistant, config: Union[ConfigType, Template] +) -> Union[ConfigType, Template]: """Validate config.""" + if isinstance(config, Template): + return config + condition = config[CONF_CONDITION] if condition in ("and", "not", "or"): conditions = [] @@ -597,6 +604,7 @@ async def async_validate_condition_config( if condition == "device": config = cv.DEVICE_CONDITION_SCHEMA(config) + assert not isinstance(config, Template) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], "condition" ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index dd5a8b6522c..cc8adea81da 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1018,20 +1018,25 @@ DEVICE_CONDITION_BASE_SCHEMA = vol.Schema( DEVICE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -CONDITION_SCHEMA: vol.Schema = key_value_schemas( - CONF_CONDITION, - { - "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA, - "state": STATE_CONDITION_SCHEMA, - "sun": SUN_CONDITION_SCHEMA, - "template": TEMPLATE_CONDITION_SCHEMA, - "time": TIME_CONDITION_SCHEMA, - "zone": ZONE_CONDITION_SCHEMA, - "and": AND_CONDITION_SCHEMA, - "or": OR_CONDITION_SCHEMA, - "not": NOT_CONDITION_SCHEMA, - "device": DEVICE_CONDITION_SCHEMA, - }, +CONDITION_SCHEMA: vol.Schema = vol.Schema( + vol.Any( + key_value_schemas( + CONF_CONDITION, + { + "numeric_state": NUMERIC_STATE_CONDITION_SCHEMA, + "state": STATE_CONDITION_SCHEMA, + "sun": SUN_CONDITION_SCHEMA, + "template": TEMPLATE_CONDITION_SCHEMA, + "time": TIME_CONDITION_SCHEMA, + "zone": ZONE_CONDITION_SCHEMA, + "and": AND_CONDITION_SCHEMA, + "or": OR_CONDITION_SCHEMA, + "not": NOT_CONDITION_SCHEMA, + "device": DEVICE_CONDITION_SCHEMA, + }, + ), + dynamic_template, + ) ) TRIGGER_SCHEMA = vol.All( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d4d9c11fa71..604102e6af3 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -935,7 +935,10 @@ class Script: await asyncio.shield(self._async_stop(update_state)) async def _async_get_condition(self, config): - config_cache_key = frozenset((k, str(v)) for k, v in config.items()) + if isinstance(config, template.Template): + config_cache_key = config.template + else: + config_cache_key = frozenset((k, str(v)) for k, v in config.items()) cond = self._config_cache.get(config_cache_key) if not cond: cond = await condition.async_from_config(self._hass, config, False) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 8bbe28d3003..3952e781952 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -232,6 +232,31 @@ async def test_two_conditions_with_and(hass, calls): assert len(calls) == 1 +async def test_shorthand_conditions_template(hass, calls): + """Test shorthand nation form in conditions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": [{"platform": "event", "event_type": "test_event"}], + "condition": "{{ is_state('test.entity', 'hello') }}", + "action": {"service": "test.automation"}, + } + }, + ) + + hass.states.async_set("test.entity", "hello") + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set("test.entity", "goodbye") + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_automation_list_setting(hass, calls): """Event is not a valid condition.""" assert await async_setup_component( diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index afe1c294290..dcd652913e5 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -128,10 +128,7 @@ async def test_or_condition_with_template(hass): { "condition": "or", "conditions": [ - { - "condition": "template", - "value_template": '{{ states.sensor.temperature.state == "100" }}', - }, + {'{{ states.sensor.temperature.state == "100" }}'}, { "condition": "numeric_state", "entity_id": "sensor.temperature", diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index e997e3e92d6..d298283d11e 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -982,7 +982,8 @@ async def test_repeat_count(hass): @pytest.mark.parametrize("condition", ["while", "until"]) -async def test_repeat_conditional(hass, condition): +@pytest.mark.parametrize("direct_template", [False, True]) +async def test_repeat_conditional(hass, condition, direct_template): """Test repeat action w/ while option.""" event = "test_event" events = async_capture_events(hass, event) @@ -1004,15 +1005,23 @@ async def test_repeat_conditional(hass, condition): } } if condition == "while": - sequence["repeat"]["while"] = { - "condition": "template", - "value_template": "{{ not is_state('sensor.test', 'done') }}", - } + template = "{{ not is_state('sensor.test', 'done') }}" + if direct_template: + sequence["repeat"]["while"] = template + else: + sequence["repeat"]["while"] = { + "condition": "template", + "value_template": template, + } else: - sequence["repeat"]["until"] = { - "condition": "template", - "value_template": "{{ is_state('sensor.test', 'done') }}", - } + template = "{{ is_state('sensor.test', 'done') }}" + if direct_template: + sequence["repeat"]["until"] = template + else: + sequence["repeat"]["until"] = { + "condition": "template", + "value_template": template, + } script_obj = script.Script( hass, cv.SCRIPT_SCHEMA(sequence), "Test Name", "test_domain" ) @@ -1193,10 +1202,7 @@ async def test_choose(hass, var, result): "sequence": {"event": event, "event_data": {"choice": "first"}}, }, { - "conditions": { - "condition": "template", - "value_template": "{{ var == 2 }}", - }, + "conditions": "{{ var == 2 }}", "sequence": {"event": event, "event_data": {"choice": "second"}}, }, ], From af47a94e70b91dff9f24933f89d93cad5ddccf8d Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Sun, 6 Sep 2020 17:22:52 +0200 Subject: [PATCH 682/862] Update py-melissa-climate to 2.1.4 (#39708) * Bumping melissa version Took 48 minutes * Black Took 6 minutes Co-authored-by: magnusknutas --- homeassistant/components/melissa/climate.py | 4 +++- homeassistant/components/melissa/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index abbc15c936f..72b3393bf36 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -170,7 +170,9 @@ class MelissaClimate(ClimateEntity): self._cur_settings.update(value) except AttributeError: old_value = None - if not await self._api.async_send(self._serial_number, self._cur_settings): + if not await self._api.async_send( + self._serial_number, "melissa", self._cur_settings + ): self._cur_settings = old_value async def async_update(self): diff --git a/homeassistant/components/melissa/manifest.json b/homeassistant/components/melissa/manifest.json index af29b0382c5..b4e1881c4d0 100644 --- a/homeassistant/components/melissa/manifest.json +++ b/homeassistant/components/melissa/manifest.json @@ -2,6 +2,6 @@ "domain": "melissa", "name": "Melissa", "documentation": "https://www.home-assistant.io/integrations/melissa", - "requirements": ["py-melissa-climate==2.0.0"], + "requirements": ["py-melissa-climate==2.1.4"], "codeowners": ["@kennedyshead"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5dce4e3645d..8f1e71e141a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1167,7 +1167,7 @@ py-canary==0.5.0 py-cpuinfo==7.0.0 # homeassistant.components.melissa -py-melissa-climate==2.0.0 +py-melissa-climate==2.1.4 # homeassistant.components.nightscout py-nightscout==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44aad4fd6e1..eb76f07a6ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ py-august==0.25.0 py-canary==0.5.0 # homeassistant.components.melissa -py-melissa-climate==2.0.0 +py-melissa-climate==2.1.4 # homeassistant.components.nightscout py-nightscout==1.2.1 From f29154011ebe8f8209b6f3b1628df4583e064927 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 6 Sep 2020 17:34:51 +0200 Subject: [PATCH 683/862] Bump aioshelly library to 0.3.0 (#39716) --- homeassistant/components/shelly/__init__.py | 11 ++++++++--- homeassistant/components/shelly/config_flow.py | 7 ++++--- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 8c83d9ee6e9..85c8879756f 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -37,13 +37,18 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Shelly from a config entry.""" + temperature_unit = "C" if hass.config.units.is_metric else "F" + options = aioshelly.ConnectionOptions( + entry.data[CONF_HOST], + entry.data.get(CONF_USERNAME), + entry.data.get(CONF_PASSWORD), + temperature_unit, + ) try: async with async_timeout.timeout(5): device = await aioshelly.Device.create( - entry.data[CONF_HOST], aiohttp_client.async_get_clientsession(hass), - entry.data.get(CONF_USERNAME), - entry.data.get(CONF_PASSWORD), + options, ) except (asyncio.TimeoutError, OSError) as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index cd35f7d2552..6446d2dd2d2 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -25,12 +25,13 @@ async def validate_input(hass: core.HomeAssistant, host, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ + options = aioshelly.ConnectionOptions( + host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD) + ) async with async_timeout.timeout(5): device = await aioshelly.Device.create( - host, aiohttp_client.async_get_clientsession(hass), - data.get(CONF_USERNAME), - data.get(CONF_PASSWORD), + options, ) await device.shutdown() diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index f15c4fb3f0d..010ecf16a56 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly2", - "requirements": ["aioshelly==0.2.1"], + "requirements": ["aioshelly==0.3.0"], "zeroconf": ["_http._tcp.local."], "codeowners": ["@balloob", "@bieniu"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8f1e71e141a..2ecbe135cae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.2.1 +aioshelly==0.3.0 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb76f07a6ac..ab27fae3caa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.2.1 +aioshelly==0.3.0 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From d7e471e244a4d71b22081acc2907f9a8cfe19582 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Sep 2020 12:56:38 -0500 Subject: [PATCH 684/862] Add HomeKit discovery for iSmartGate (#39702) --- .../components/gogogate2/config_flow.py | 26 +++++- .../components/gogogate2/manifest.json | 7 +- homeassistant/generated/zeroconf.py | 1 + .../components/gogogate2/test_config_flow.py | 83 ++++++++++++++++++- 4 files changed, 112 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index 4a70a822023..3642e28ccac 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -29,12 +29,32 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + def __init__(self): + """Initialize the config flow.""" + self._ip_address = None + self._device_type = None + async def async_step_import(self, config_data: dict = None): """Handle importing of configuration.""" result = await self.async_step_user(config_data) self._abort_if_unique_id_configured() return result + async def async_step_homekit(self, discovery_info): + """Handle homekit discovery.""" + await self.async_set_unique_id(discovery_info["properties"]["id"]) + self._abort_if_unique_id_configured({CONF_IP_ADDRESS: discovery_info["host"]}) + + ip_address = discovery_info["host"] + + for entry in self._async_current_entries(): + if entry.data.get(CONF_IP_ADDRESS) == ip_address: + return self.async_abort(reason="already_configured") + + self._ip_address = ip_address + self._device_type = DEVICE_TYPE_ISMARTGATE + return await self.async_step_user() + async def async_step_user(self, user_input: dict = None): """Handle user initiated flow.""" user_input = user_input or {} @@ -88,10 +108,12 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): { vol.Required( CONF_DEVICE, - default=user_input.get(CONF_DEVICE, DEVICE_TYPE_GOGOGATE2), + default=self._device_type + or user_input.get(CONF_DEVICE, DEVICE_TYPE_GOGOGATE2), ): vol.In((DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE)), vol.Required( - CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS, "") + CONF_IP_ADDRESS, + default=user_input.get(CONF_IP_ADDRESS, self._ip_address), ): str, vol.Required( CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 08c8c64b139..b6921991ecc 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -4,5 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", "requirements": ["gogogate2-api==2.0.0"], - "codeowners": ["@vangorra"] + "codeowners": ["@vangorra"], + "homekit": { + "models": [ + "iSmartGate" + ] + } } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ba12b4ec4de..ea61ccfbaeb 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -92,5 +92,6 @@ HOMEKIT = { "TRADFRI": "tradfri", "Welcome": "netatmo", "Wemo": "wemo", + "iSmartGate": "gogogate2", "tado": "tado" } diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 8c07a3fc023..667c0330d80 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -3,7 +3,12 @@ from gogogate2_api import GogoGate2Api from gogogate2_api.common import ApiError from gogogate2_api.const import GogoGate2ApiErrorCode -from homeassistant.components.gogogate2.const import DEVICE_TYPE_GOGOGATE2 +from homeassistant import config_entries, setup +from homeassistant.components.gogogate2.const import ( + DEVICE_TYPE_GOGOGATE2, + DEVICE_TYPE_ISMARTGATE, + DOMAIN, +) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_DEVICE, @@ -12,9 +17,12 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry + +MOCK_MAC_ADDR = "AA:BB:CC:DD:EE:FF" @patch("homeassistant.components.gogogate2.async_setup", return_value=True) @@ -64,3 +72,74 @@ async def test_auth_fail( assert result assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_homekit_unique_id_already_setup(hass): + """Test that we abort from homekit if gogogate2 is already setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + flow = next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert flow["context"]["unique_id"] == MOCK_MAC_ADDR + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_IP_ADDRESS: "1.2.3.4", CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + ) + assert result["type"] == RESULT_TYPE_ABORT + + +async def test_form_homekit_ip_address_already_setup(hass): + """Test that we abort from homekit if gogogate2 is already setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_IP_ADDRESS: "1.2.3.4", CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + ) + assert result["type"] == RESULT_TYPE_ABORT + + +async def test_form_homekit_ip_address(hass): + """Test homekit includes the defaults ip address.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + data_schema = result["data_schema"] + assert data_schema({CONF_USERNAME: "username", CONF_PASSWORD: "password"}) == { + CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, + CONF_IP_ADDRESS: "1.2.3.4", + CONF_PASSWORD: "password", + CONF_USERNAME: "username", + } From 878347243d6125765b946d21bddd4fd4124e614f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 6 Sep 2020 20:04:07 +0200 Subject: [PATCH 685/862] Numeric state condition can also accept input_number entity ID (#39680) --- homeassistant/helpers/condition.py | 34 +++++++++---- homeassistant/helpers/config_validation.py | 8 +++- tests/helpers/test_condition.py | 56 ++++++++++++++++++++++ 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 82839d1e0f8..473641b10a4 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -171,8 +171,8 @@ async def async_not_from_config( def numeric_state( hass: HomeAssistant, entity: Union[None, str, State], - below: Optional[float] = None, - above: Optional[float] = None, + below: Optional[Union[float, str]] = None, + above: Optional[Union[float, str]] = None, value_template: Optional[Template] = None, variables: TemplateVarsType = None, ) -> bool: @@ -192,8 +192,8 @@ def numeric_state( def async_numeric_state( hass: HomeAssistant, entity: Union[None, str, State], - below: Optional[float] = None, - above: Optional[float] = None, + below: Optional[Union[float, str]] = None, + above: Optional[Union[float, str]] = None, value_template: Optional[Template] = None, variables: TemplateVarsType = None, attribute: Optional[str] = None, @@ -233,11 +233,29 @@ def async_numeric_state( ) return False - if below is not None and fvalue >= below: - return False + if below is not None: + if isinstance(below, str): + below_entity = hass.states.get(below) + if ( + not below_entity + or below_entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) + or fvalue >= float(below_entity.state) + ): + return False + elif fvalue >= below: + return False - if above is not None and fvalue <= above: - return False + if above is not None: + if isinstance(above, str): + above_entity = hass.states.get(above) + if ( + not above_entity + or above_entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN) + or fvalue <= float(above_entity.state) + ): + return False + elif fvalue <= above: + return False return True diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index cc8adea81da..c3842c538d8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -906,8 +906,12 @@ NUMERIC_STATE_CONDITION_SCHEMA = vol.All( vol.Required(CONF_CONDITION): "numeric_state", vol.Required(CONF_ENTITY_ID): entity_ids, vol.Optional(CONF_ATTRIBUTE): str, - CONF_BELOW: vol.Coerce(float), - CONF_ABOVE: vol.Coerce(float), + CONF_BELOW: vol.Any( + vol.Coerce(float), vol.All(str, entity_domain("input_number")) + ), + CONF_ABOVE: vol.Any( + vol.Coerce(float), vol.All(str, entity_domain("input_number")) + ), vol.Optional(CONF_VALUE_TEMPLATE): template, } ), diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index dcd652913e5..3f25ab598d5 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -505,6 +505,62 @@ async def test_numberic_state_attribute(hass): assert not test(hass) +async def test_numeric_state_using_input_number(hass): + """Test numeric_state conditions using input_number entities.""" + await async_setup_component( + hass, + "input_number", + { + "input_number": { + "low": {"min": 0, "max": 255, "initial": 10}, + "high": {"min": 0, "max": 255, "initial": 100}, + } + }, + ) + + test = await condition.async_from_config( + hass, + { + "condition": "and", + "conditions": [ + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": "input_number.high", + "above": "input_number.low", + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 42) + assert test(hass) + + hass.states.async_set("sensor.temperature", 10) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100) + assert not test(hass) + + await hass.services.async_call( + "input_number", + "set_value", + { + "entity_id": "input_number.high", + "value": 101, + }, + blocking=True, + ) + assert test(hass) + + assert not condition.async_numeric_state( + hass, entity="sensor.temperature", below="input_number.not_exist" + ) + assert not condition.async_numeric_state( + hass, entity="sensor.temperature", above="input_number.not_exist" + ) + + async def test_zone_multiple_entities(hass): """Test with multiple entities in condition.""" test = await condition.async_from_config( From c9a4deb1184834329b502a8343f3a44aa57c1c35 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 7 Sep 2020 02:46:36 +0800 Subject: [PATCH 686/862] Set log level for libav.mp4 in stream (#39719) Also add @uvjustin to codeowners --- CODEOWNERS | 2 +- homeassistant/components/stream/__init__.py | 1 + homeassistant/components/stream/manifest.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0f48fcc3dcc..6230904de7b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -408,7 +408,7 @@ homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stookalert/* @fwestenberg -homeassistant/components/stream/* @hunterjm +homeassistant/components/stream/* @hunterjm @uvjustin homeassistant/components/stt/* @pvizeli homeassistant/components/suez_water/* @ooii homeassistant/components/sun/* @Swamp-Ig diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 3ce3d0af6fd..d754f0beb01 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -82,6 +82,7 @@ async def async_setup(hass, config): """Set up stream.""" # Set log level to error for libav logging.getLogger("libav").setLevel(logging.ERROR) + logging.getLogger("libav.mp4").setLevel(logging.ERROR) # Keep import here so that we can import stream integration without installing reqs # pylint: disable=import-outside-toplevel diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index d434d1ce1ed..3d194bdf0d4 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/stream", "requirements": ["av==8.0.2"], "dependencies": ["http"], - "codeowners": ["@hunterjm"], + "codeowners": ["@hunterjm", "@uvjustin"], "quality_scale": "internal" } From 8af64456ec8995c2e584bacd85ba1bca5f219b6f Mon Sep 17 00:00:00 2001 From: fillefilip8 Date: Sun, 6 Sep 2020 20:49:54 +0200 Subject: [PATCH 687/862] Add number parsing for OpenHardwareMonitor (#39030) --- .../components/openhardwaremonitor/sensor.py | 15 ++++++++++++--- .../components/openhardwaremonitor/test_sensor.py | 11 +++++++++-- tests/fixtures/openhardwaremonitor.json | 6 +++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index c0c71405ab3..115366dac66 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -77,6 +77,11 @@ class OpenHardwareMonitorDevice(Entity): """Return the state attributes of the sun.""" return self.attributes + @classmethod + def parse_number(cls, string): + """In some locales a decimal numbers uses ',' instead of '.'.""" + return string.replace(",", ".") + def update(self): """Update the device from a new JSON object.""" self._data.update() @@ -89,12 +94,16 @@ class OpenHardwareMonitorDevice(Entity): values = array[path_number] if path_index == len(self.path) - 1: - self.value = values[OHM_VALUE].split(" ")[0] + self.value = self.parse_number(values[OHM_VALUE].split(" ")[0]) _attributes.update( { "name": values[OHM_NAME], - STATE_MIN_VALUE: values[OHM_MIN].split(" ")[0], - STATE_MAX_VALUE: values[OHM_MAX].split(" ")[0], + STATE_MIN_VALUE: self.parse_number( + values[OHM_MIN].split(" ")[0] + ), + STATE_MAX_VALUE: self.parse_number( + values[OHM_MAX].split(" ")[0] + ), } ) diff --git a/tests/components/openhardwaremonitor/test_sensor.py b/tests/components/openhardwaremonitor/test_sensor.py index 6c8776f740d..3cf5ba84a25 100644 --- a/tests/components/openhardwaremonitor/test_sensor.py +++ b/tests/components/openhardwaremonitor/test_sensor.py @@ -41,8 +41,15 @@ class TestOpenHardwareMonitorSetup(unittest.TestCase): assert len(entities) == 38 state = self.hass.states.get( - "sensor.test_pc_intel_core_i7_7700_clocks_bus_speed" + "sensor.test_pc_intel_core_i7_7700_temperatures_cpu_core_1" ) assert state is not None - assert state.state == "100" + assert state.state == "31.0" + + state = self.hass.states.get( + "sensor.test_pc_intel_core_i7_7700_temperatures_cpu_core_2" + ) + + assert state is not None + assert state.state == "30.0" diff --git a/tests/fixtures/openhardwaremonitor.json b/tests/fixtures/openhardwaremonitor.json index 13c5b5481e0..960da03f535 100644 --- a/tests/fixtures/openhardwaremonitor.json +++ b/tests/fixtures/openhardwaremonitor.json @@ -91,9 +91,9 @@ "id": 12, "Text": "CPU Core #2", "Children": [], - "Min": "29.0 °C", - "Value": "30.0 °C", - "Max": "61.0 °C", + "Min": "29,0 °C", + "Value": "30,0 °C", + "Max": "61,0 °C", "ImageURL": "images/transparent.png" }, { From f2d21ea735c80242847972ece7a8902495a58826 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 6 Sep 2020 15:00:28 -0400 Subject: [PATCH 688/862] Update ZHA storage every 10 minutes (#39710) --- homeassistant/components/zha/core/gateway.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 1f58dc65086..bb57d7f03f4 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -26,6 +26,7 @@ from homeassistant.helpers.entity_registry import ( async_entries_for_device, async_get_registry as get_ent_reg, ) +from homeassistant.helpers.event import async_track_time_interval from . import discovery, typing as zha_typing from .const import ( @@ -117,6 +118,7 @@ class ZHAGateway: self.debug_enabled = False self._log_relay_handler = LogRelayHandler(hass, self) self._config_entry = config_entry + self._unsubs = [] async def async_initialize(self): """Initialize controller and connect radio.""" @@ -186,6 +188,13 @@ class ZHAGateway: "available" if zha_device.available else "unavailable", delta_msg, ) + # update the last seen time for devices every 10 minutes to avoid thrashing + # writes and shutdown issues where storage isn't updated + self._unsubs.append( + async_track_time_interval( + self._hass, self.async_update_device_storage, timedelta(minutes=10) + ) + ) @callback def async_load_groups(self) -> None: @@ -515,7 +524,7 @@ class ZHAGateway: if device.status is DeviceStatus.INITIALIZED: device.update_available(available) - async def async_update_device_storage(self): + async def async_update_device_storage(self, *_): """Update the devices in the store.""" for device in self.devices.values(): self.zha_storage.async_update_device(device) @@ -626,6 +635,8 @@ class ZHAGateway: async def shutdown(self): """Stop ZHA Controller Application.""" _LOGGER.debug("Shutting down ZHA ControllerApplication") + for unsubscribe in self._unsubs: + unsubscribe() await self.application_controller.shutdown() From 61e8ab13009551a6a9cf2ba39c0719e767487b18 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 6 Sep 2020 22:26:14 +0200 Subject: [PATCH 689/862] Add Plugwise scan_interval config option (#37229) Co-authored-by: Tom Scholten Co-authored-by: Tom Scholten --- homeassistant/components/plugwise/__init__.py | 31 +++- .../components/plugwise/binary_sensor.py | 11 +- homeassistant/components/plugwise/climate.py | 11 +- .../components/plugwise/config_flow.py | 43 ++++- homeassistant/components/plugwise/const.py | 3 + homeassistant/components/plugwise/sensor.py | 3 +- .../components/plugwise/strings.json | 10 ++ homeassistant/components/plugwise/switch.py | 4 +- tests/components/plugwise/test_config_flow.py | 170 ++++++++++++++++-- 9 files changed, 255 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 3cfff3a8521..8c140f65af9 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -10,6 +10,7 @@ import async_timeout import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -20,7 +21,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DOMAIN +from .const import COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, UNDO_UPDATE_LISTENER CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -39,7 +40,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Plugwise Smiles from a config entry.""" websession = async_get_clientsession(hass, verify_ssl=False) api = Smile( - host=entry.data["host"], password=entry.data["password"], websession=websession + host=entry.data[CONF_HOST], + password=entry.data[CONF_PASSWORD], + websession=websession, ) try: @@ -61,9 +64,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Timeout while connecting to Smile") raise ConfigEntryNotReady from err - update_interval = timedelta(seconds=60) - if api.smile_type == "power": - update_interval = timedelta(seconds=10) + update_interval = timedelta( + seconds=entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL[api.smile_type] + ) + ) async def async_update_data(): """Update data via API endpoint.""" @@ -89,9 +94,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.get_all_devices() + undo_listener = entry.add_update_listener(_update_listener) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { "api": api, - "coordinator": coordinator, + COORDINATOR: coordinator, + UNDO_UPDATE_LISTENER: undo_listener, } device_registry = await dr.async_get_registry(hass) @@ -118,6 +126,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] + coordinator.update_interval = timedelta( + seconds=entry.options.get(CONF_SCAN_INTERVAL) + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( @@ -128,6 +144,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ] ) ) + + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index d6b6424c7ce..67dcc10a289 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -6,7 +6,14 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback -from .const import DOMAIN, FLAME_ICON, FLOW_OFF_ICON, FLOW_ON_ICON, IDLE_ICON +from .const import ( + COORDINATOR, + DOMAIN, + FLAME_ICON, + FLOW_OFF_ICON, + FLOW_ON_ICON, + IDLE_ICON, +) from .sensor import SmileSensor BINARY_SENSOR_MAP = { @@ -20,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Smile binary_sensors from a config entry.""" api = hass.data[DOMAIN][config_entry.entry_id]["api"] - coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] entities = [] diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index dbc9e54e0d7..c6e34ed46cb 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -19,7 +19,14 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from . import SmileGateway -from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SCHEDULE_OFF, SCHEDULE_ON +from .const import ( + COORDINATOR, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + DOMAIN, + SCHEDULE_OFF, + SCHEDULE_ON, +) HVAC_MODES_HEAT_ONLY = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] HVAC_MODES_HEAT_COOL = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO] @@ -32,7 +39,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Smile Thermostats from a config entry.""" api = hass.data[DOMAIN][config_entry.entry_id]["api"] - coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] entities = [] thermostat_classes = [ diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 1f86394775a..4d1752a2774 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -5,11 +5,12 @@ from Plugwise_Smile.Smile import Smile import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL +from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType -from .const import DOMAIN # pylint:disable=unused-import +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -56,7 +57,7 @@ async def validate_input(hass: core.HomeAssistant, data): return api -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Plugwise Smile.""" VERSION = 1 @@ -98,7 +99,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: api = await validate_input(self.hass, user_input) - return self.async_create_entry(title=api.smile_name, data=user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -109,13 +109,46 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not errors: await self.async_set_unique_id(api.gateway_id) + self._abort_if_unique_id_configured() return self.async_create_entry(title=api.smile_name, data=user_input) return self.async_show_form( - step_id="user", data_schema=_base_schema(self.discovery_info), errors=errors + step_id="user", + data_schema=_base_schema(self.discovery_info), + errors=errors or {}, ) + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return PlugwiseOptionsFlowHandler(config_entry) + + +class PlugwiseOptionsFlowHandler(config_entries.OptionsFlow): + """Plugwise option flow.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Plugwise options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + api = self.hass.data[DOMAIN][self.config_entry.entry_id]["api"] + interval = DEFAULT_SCAN_INTERVAL[api.smile_type] + data = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get(CONF_SCAN_INTERVAL, interval), + ): int + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(data)) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 9dc4c24b1e1..6feceff2d3c 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -39,3 +39,6 @@ FLAME_ICON = "mdi:fire" IDLE_ICON = "mdi:circle-off-outline" FLOW_OFF_ICON = "mdi:water-pump-off" FLOW_ON_ICON = "mdi:water-pump" + +UNDO_UPDATE_LISTENER = "undo_update_listener" +COORDINATOR = "coordinator" diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 1e1cb607ff4..787f4630001 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -22,6 +22,7 @@ from homeassistant.helpers.entity import Entity from . import SmileGateway from .const import ( COOL_ICON, + COORDINATOR, DEVICE_STATE, DOMAIN, FLAME_ICON, @@ -168,7 +169,7 @@ CUSTOM_ICONS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Smile sensors from a config entry.""" api = hass.data[DOMAIN][config_entry.entry_id]["api"] - coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] entities = [] all_devices = api.get_all_devices() diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 70c1d127390..7dc8542698b 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -1,4 +1,14 @@ { + "options": { + "step": { + "init": { + "description": "Adjust Plugwise Options", + "data": { + "scan_interval": "Scan Interval (seconds)" + } + } + } + }, "config": { "step": { "user": { diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index bd831e2f9aa..a34eabe3d2e 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -8,7 +8,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback from . import SmileGateway -from .const import DOMAIN +from .const import COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Smile switches from a config entry.""" api = hass.data[DOMAIN][config_entry.entry_id]["api"] - coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] entities = [] all_devices = api.get_all_devices() diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 4d8a78f11c8..6044381ac51 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -2,10 +2,28 @@ from Plugwise_Smile.Smile import Smile import pytest -from homeassistant import config_entries, setup -from homeassistant.components.plugwise.const import DOMAIN +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.plugwise import config_flow +from homeassistant.components.plugwise.const import DEFAULT_SCAN_INTERVAL, DOMAIN +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL -from tests.async_mock import patch +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry + +TEST_HOST = "1.1.1.1" +TEST_HOSTNAME = "smileabcdef" +TEST_PASSWORD = "test_password" +TEST_DISCOVERY = { + "host": TEST_HOST, + "hostname": f"{TEST_HOSTNAME}.local.", + "server": f"{TEST_HOSTNAME}.local.", + "properties": { + "product": "smile", + "version": "1.2.3", + "hostname": f"{TEST_HOSTNAME}.local.", + }, +} @pytest.fixture(name="mock_smile") @@ -25,9 +43,9 @@ 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} + DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -42,15 +60,56 @@ async def test_form(hass): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"host": "1.1.1.1", "password": "test-password"}, + {"host": TEST_HOST, "password": TEST_PASSWORD}, ) - assert result2["type"] == "create_entry" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["data"] == { - "host": "1.1.1.1", - "password": "test-password", + "host": TEST_HOST, + "password": TEST_PASSWORD, } await hass.async_block_till_done() + + assert result["errors"] == {} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_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": SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.plugwise.config_flow.Smile.connect", + return_value=True, + ), patch( + "homeassistant.components.plugwise.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.plugwise.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": TEST_PASSWORD}, + ) + + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + "host": TEST_HOST, + "password": TEST_PASSWORD, + } + + assert result["errors"] == {} assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -66,10 +125,10 @@ async def test_form_invalid_auth(hass, mock_smile): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"host": "1.1.1.1", "password": "test-password"}, + {"host": TEST_HOST, "password": TEST_PASSWORD}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -84,8 +143,93 @@ async def test_form_cannot_connect(hass, mock_smile): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"host": "1.1.1.1", "password": "test-password"}, + {"host": TEST_HOST, "password": TEST_PASSWORD}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_other_problem(hass, mock_smile): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_smile.connect.side_effect = TimeoutError + mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": TEST_HOST, "password": TEST_PASSWORD}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_show_zeroconf_form(hass, mock_smile) -> None: + """Test that the zeroconf confirmation form is served.""" + flow = config_flow.PlugwiseConfigFlow() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf(TEST_DISCOVERY) + + await hass.async_block_till_done() + assert flow.context["title_placeholders"][CONF_HOST] == TEST_HOST + assert flow.context["title_placeholders"]["name"] == "P1 DSMR v1.2.3" + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_options_flow_power(hass, mock_smile) -> None: + """Test config flow options DSMR environments.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CONF_NAME, + data={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + + hass.data[DOMAIN] = {entry.entry_id: {"api": MagicMock(smile_type="power")}} + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 10} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_SCAN_INTERVAL: 10, + } + + +async def test_options_flow_thermo(hass, mock_smile) -> None: + """Test config flow options for thermostatic environments.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CONF_NAME, + data={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + + hass.data[DOMAIN] = {entry.entry_id: {"api": MagicMock(smile_type="thermostat")}} + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 60} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_SCAN_INTERVAL: 60, + } From e649e27d4d6941c13135bbc6f067c194d9d8d3ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Sep 2020 15:40:27 -0500 Subject: [PATCH 690/862] Bump zeroconf to resolve a performance issue with the cache reaper (#39713) https://github.com/jstasiak/python-zeroconf/pull/297 --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 787422ff1c2..c703283e38d 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.3"], + "requirements": ["zeroconf==0.28.4"], "dependencies": ["api"], "codeowners": ["@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a21eade4aaa..983535ad7ce 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ sqlalchemy==1.3.19 voluptuous-serialize==2.4.0 voluptuous==0.11.7 yarl==1.4.2 -zeroconf==0.28.3 +zeroconf==0.28.4 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 2ecbe135cae..0ee2a96cf59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2283,7 +2283,7 @@ youtube_dl==2020.07.28 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.3 +zeroconf==0.28.4 # homeassistant.components.zha zha-quirks==0.0.43 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab27fae3caa..f72412122b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1053,7 +1053,7 @@ xmltodict==0.12.0 yeelight==0.5.3 # homeassistant.components.zeroconf -zeroconf==0.28.3 +zeroconf==0.28.4 # homeassistant.components.zha zha-quirks==0.0.43 From dd00cfc7a2bfe880b53f24723dbdef977af2cf16 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 6 Sep 2020 22:48:07 +0200 Subject: [PATCH 691/862] Fix media browser panel title (#39720) --- homeassistant/components/media_source/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 22f633d21d0..3dff949d5dd 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -38,7 +38,7 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.components.websocket_api.async_register_command(websocket_browse_media) hass.components.websocket_api.async_register_command(websocket_resolve_media) hass.components.frontend.async_register_built_in_panel( - "media-browser", "media-browser", "hass:play-box-multiple" + "media-browser", "media_browser", "hass:play-box-multiple" ) local_source.async_setup(hass) await async_process_integration_platforms( From f828cdcaef302afd9b52a14fe450dd58d1cf29dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 6 Sep 2020 23:52:06 +0300 Subject: [PATCH 692/862] Fix setup of ONVIF devices without snapshot capability (#39723) * Fix setup of ONVIF devices without snapshot capability * Unrelated debug message typo fix --- homeassistant/components/onvif/__init__.py | 2 ++ homeassistant/components/onvif/device.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 93e948b770b..964c7a70a6d 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -127,6 +127,8 @@ async def _get_snapshot_auth(hass, device, entry): return HTTP_DIGEST_AUTHENTICATION snapshot_uri = await device.async_get_snapshot_uri(device.profiles[0]) + if not snapshot_uri: + return HTTP_DIGEST_AUTHENTICATION auth = HTTPDigestAuth(device.username, device.password) def _get(): diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 1a879e54767..1dfa670114f 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -217,7 +217,7 @@ class ONVIFDevice: raise fault LOGGER.debug( - "Couldn't get network interfaces from ONVIF deivice '%s'. Error: %s", + "Couldn't get network interfaces from ONVIF device '%s'. Error: %s", self.name, fault, ) From 19818d96b74ec2abfdaf2234fe365118e33e5f04 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 6 Sep 2020 22:55:29 +0200 Subject: [PATCH 693/862] Spotify browser add more sources (#39296) Co-authored-by: Tobias Sauerwein --- .../components/spotify/manifest.json | 2 +- .../components/spotify/media_player.py | 169 +++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 127 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 88e0c938a28..d17cd43c47f 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.12.0"], + "requirements": ["spotipy==2.14.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 7f7659061e8..ccc51ad41a6 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -13,6 +13,7 @@ from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, + MEDIA_TYPE_EPISODE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TRACK, @@ -70,19 +71,29 @@ SUPPORT_SPOTIFY = ( BROWSE_LIMIT = 48 +MEDIA_TYPE_SHOW = "show" + PLAYABLE_MEDIA_TYPES = [ MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_SHOW, MEDIA_TYPE_TRACK, ] LIBRARY_MAP = { - "user_playlists": "Playlists", + "current_user_playlists": "Playlists", + "current_user_followed_artists": "Artists", + "current_user_saved_albums": "Albums", + "current_user_saved_tracks": "Tracks", + "current_user_saved_shows": "Podcasts", + "current_user_recently_played": "Recently played", + "current_user_top_artists": "Top Artists", + "current_user_top_tracks": "Top Tracks", + "categories": "Categories", "featured_playlists": "Featured Playlists", "new_releases": "New Releases", - "current_user_top_artists": "Top Artists", - "current_user_recently_played": "Recently played", } @@ -233,7 +244,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): or not self._currently_playing["item"]["album"]["images"] ): return None - return self._currently_playing["item"]["album"]["images"][0]["url"] + return fetch_image_url(self._currently_playing["item"]["album"]) @property def media_image_remotely_accessible(self) -> bool: @@ -338,7 +349,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): # Yet, they do generate those types of URI in their official clients. media_id = str(URL(media_id).with_query(None).with_fragment(None)) - if media_type in (MEDIA_TYPE_TRACK, MEDIA_TYPE_MUSIC): + if media_type in (MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MUSIC): kwargs["uris"] = [media_id] elif media_type in PLAYABLE_MEDIA_TYPES: kwargs["context_uri"] = media_id @@ -346,6 +357,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity): _LOGGER.error("Media type %s is not supported", media_type) return + if not self._currently_playing.get("device") and self._devices: + kwargs["device_id"] = self._devices[0].get("id") + self._spotify.start_playback(**kwargs) @spotify_exception_handler @@ -388,6 +402,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + if not self._scope_ok: raise NotImplementedError @@ -399,7 +414,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): "media_content_id": media_content_id, } response = await self.hass.async_add_executor_job( - build_item_response, self._spotify, payload + build_item_response, self._spotify, self._me, payload ) if response is None: raise BrowseError( @@ -408,34 +423,72 @@ class SpotifyMediaPlayer(MediaPlayerEntity): return response -def build_item_response(spotify, payload): +def build_item_response(spotify, user, payload): """Create response payload for the provided media query.""" media_content_type = payload.get("media_content_type") + media_content_id = payload.get("media_content_id") title = None - if media_content_type == "user_playlists": + image = None + if media_content_type == "current_user_playlists": media = spotify.current_user_playlists(limit=BROWSE_LIMIT) items = media.get("items", []) + elif media_content_type == "current_user_followed_artists": + media = spotify.current_user_followed_artists(limit=BROWSE_LIMIT) + items = media.get("artists", {}).get("items", []) + elif media_content_type == "current_user_saved_albums": + media = spotify.current_user_saved_albums(limit=BROWSE_LIMIT) + items = media.get("items", []) + elif media_content_type == "current_user_saved_tracks": + media = spotify.current_user_saved_tracks(limit=BROWSE_LIMIT) + items = media.get("items", []) + elif media_content_type == "current_user_saved_shows": + media = spotify.current_user_saved_shows(limit=BROWSE_LIMIT) + items = media.get("items", []) elif media_content_type == "current_user_recently_played": media = spotify.current_user_recently_played(limit=BROWSE_LIMIT) items = media.get("items", []) - elif media_content_type == "featured_playlists": - media = spotify.featured_playlists(limit=BROWSE_LIMIT) - items = media.get("playlists", {}).get("items", []) elif media_content_type == "current_user_top_artists": media = spotify.current_user_top_artists(limit=BROWSE_LIMIT) items = media.get("items", []) + elif media_content_type == "current_user_top_tracks": + media = spotify.current_user_top_tracks(limit=BROWSE_LIMIT) + items = media.get("items", []) + elif media_content_type == "featured_playlists": + media = spotify.featured_playlists(country=user["country"], limit=BROWSE_LIMIT) + items = media.get("playlists", {}).get("items", []) + elif media_content_type == "categories": + media = spotify.categories(country=user["country"], limit=BROWSE_LIMIT) + items = media.get("categories", {}).get("items", []) + elif media_content_type == "category_playlists": + media = spotify.category_playlists( + category_id=media_content_id, + country=user["country"], + limit=BROWSE_LIMIT, + ) + category = spotify.category(media_content_id, country=user["country"]) + title = category.get("name") + image = fetch_image_url(category, key="icons") + items = media.get("playlists", {}).get("items", []) elif media_content_type == "new_releases": - media = spotify.new_releases(limit=BROWSE_LIMIT) + media = spotify.new_releases(country=user["country"], limit=BROWSE_LIMIT) items = media.get("albums", {}).get("items", []) elif media_content_type == MEDIA_TYPE_PLAYLIST: - media = spotify.playlist(payload["media_content_id"]) + media = spotify.playlist(media_content_id) items = media.get("tracks", {}).get("items", []) elif media_content_type == MEDIA_TYPE_ALBUM: - media = spotify.album(payload["media_content_id"]) + media = spotify.album(media_content_id) items = media.get("tracks", {}).get("items", []) elif media_content_type == MEDIA_TYPE_ARTIST: - media = spotify.artist_albums(payload["media_content_id"], limit=BROWSE_LIMIT) - title = spotify.artist(payload["media_content_id"]).get("name") + media = spotify.artist_albums(media_content_id, limit=BROWSE_LIMIT) + artist = spotify.artist(media_content_id) + title = artist.get("name") + image = fetch_image_url(artist) + items = media.get("items", []) + elif media_content_type == MEDIA_TYPE_SHOW: + media = spotify.show_episodes(media_content_id, limit=BROWSE_LIMIT) + show = spotify.show(media_content_id) + title = show.get("name") + image = fetch_image_url(show) items = media.get("items", []) else: media = None @@ -444,6 +497,26 @@ def build_item_response(spotify, payload): if media is None: return None + if media_content_type == "categories": + return BrowseMedia( + title=LIBRARY_MAP.get(media_content_id), + media_content_id=media_content_id, + media_content_type=media_content_type, + can_play=False, + can_expand=True, + children=[ + BrowseMedia( + title=item.get("name"), + media_content_id=item.get("id"), + media_content_type="category_playlists", + thumbnail=fetch_image_url(item, key="icons"), + can_play=False, + can_expand=True, + ) + for item in items + ], + ) + if title is None: if "name" in media: title = media.get("name") @@ -452,15 +525,17 @@ def build_item_response(spotify, payload): response = { "title": title, - "media_content_id": payload.get("media_content_id"), - "media_content_type": payload.get("media_content_type"), - "can_play": payload.get("media_content_type") in PLAYABLE_MEDIA_TYPES, + "media_content_id": media_content_id, + "media_content_type": media_content_type, + "can_play": media_content_type in PLAYABLE_MEDIA_TYPES, "children": [item_payload(item) for item in items], "can_expand": True, } if "images" in media: response["thumbnail"] = fetch_image_url(media) + elif image: + response["thumbnail"] = image return BrowseMedia(**response) @@ -471,31 +546,35 @@ def item_payload(item): Used by async_browse_media. """ - can_expand = item.get("type") not in [None, MEDIA_TYPE_TRACK] + if MEDIA_TYPE_TRACK in item: + item = item.get(MEDIA_TYPE_TRACK) + elif MEDIA_TYPE_SHOW in item: + item = item.get(MEDIA_TYPE_SHOW) + elif MEDIA_TYPE_ARTIST in item: + item = item.get(MEDIA_TYPE_ARTIST) + elif MEDIA_TYPE_ALBUM in item and item.get("type") != MEDIA_TYPE_TRACK: + item = item.get(MEDIA_TYPE_ALBUM) - if ( - MEDIA_TYPE_TRACK in item - or item.get("type") != MEDIA_TYPE_ALBUM - and "playlists" in item - ): - track = item.get(MEDIA_TYPE_TRACK) - return BrowseMedia( - title=track.get("name"), - thumbnail=fetch_image_url(track.get(MEDIA_TYPE_ALBUM, {})), - media_content_id=track.get("uri"), - media_content_type=MEDIA_TYPE_TRACK, - can_play=True, - can_expand=can_expand, - ) + can_expand = item.get("type") not in [ + None, + MEDIA_TYPE_TRACK, + MEDIA_TYPE_EPISODE, + ] - return BrowseMedia( - title=item.get("name"), - thumbnail=fetch_image_url(item), - media_content_id=item.get("uri"), - media_content_type=item.get("type"), - can_play=item.get("type") in PLAYABLE_MEDIA_TYPES, - can_expand=can_expand, - ) + payload = { + "title": item.get("name"), + "media_content_id": item.get("uri"), + "media_content_type": item.get("type"), + "can_play": item.get("type") in PLAYABLE_MEDIA_TYPES, + "can_expand": can_expand, + } + + if "images" in item: + payload["thumbnail"] = fetch_image_url(item) + elif MEDIA_TYPE_ALBUM in item: + payload["thumbnail"] = fetch_image_url(item[MEDIA_TYPE_ALBUM]) + + return BrowseMedia(**payload) def library_payload(): @@ -522,9 +601,9 @@ def library_payload(): return library_info -def fetch_image_url(item): +def fetch_image_url(item, key="images"): """Fetch image url.""" try: - return item.get("images", [])[0].get("url") + return item.get(key, [])[0].get("url") except IndexError: - return + return None diff --git a/requirements_all.txt b/requirements_all.txt index 0ee2a96cf59..7d5dc4f3e52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2049,7 +2049,7 @@ spiderpy==1.3.1 spotcrime==1.0.4 # homeassistant.components.spotify -spotipy==2.12.0 +spotipy==2.14.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f72412122b9..2c30f58f921 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -954,7 +954,7 @@ speedtest-cli==2.1.2 spiderpy==1.3.1 # homeassistant.components.spotify -spotipy==2.12.0 +spotipy==2.14.0 # homeassistant.components.recorder # homeassistant.components.sql From 251d8919eab9c765563417a32b6c0a2e897274ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Sep 2020 16:20:32 -0500 Subject: [PATCH 694/862] Add domain filter support to async_all to match async_entity_ids (#39725) This avoids copying all the states before applying the filter --- homeassistant/components/humidifier/intent.py | 5 ++-- homeassistant/components/light/intent.py | 3 +-- .../components/owntracks/__init__.py | 5 +--- homeassistant/core.py | 22 ++++++++++++++---- homeassistant/helpers/template.py | 3 +-- tests/test_core.py | 23 +++++++++++++++++++ 6 files changed, 45 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index ee257cc7123..fafbb0a494a 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -41,8 +41,7 @@ class HumidityHandler(intent.IntentHandler): hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) state = hass.helpers.intent.async_match_state( - slots["name"]["value"], - [state for state in hass.states.async_all() if state.domain == DOMAIN], + slots["name"]["value"], hass.states.async_all(DOMAIN) ) service_data = {ATTR_ENTITY_ID: state.entity_id} @@ -87,7 +86,7 @@ class SetModeHandler(intent.IntentHandler): slots = self.async_validate_slots(intent_obj.slots) state = hass.helpers.intent.async_match_state( slots["name"]["value"], - [state for state in hass.states.async_all() if state.domain == DOMAIN], + hass.states.async_all(DOMAIN), ) service_data = {ATTR_ENTITY_ID: state.entity_id} diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index 58f74d8a422..be9346cf85b 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -39,8 +39,7 @@ class SetIntentHandler(intent.IntentHandler): hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) state = hass.helpers.intent.async_match_state( - slots["name"]["value"], - [state for state in hass.states.async_all() if state.domain == DOMAIN], + slots["name"]["value"], hass.states.async_all(DOMAIN) ) service_data = {ATTR_ENTITY_ID: state.entity_id} diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index cf034950154..24dc99de71c 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -183,10 +183,7 @@ async def handle_webhook(hass, webhook_id, request): response = [] - for person in hass.states.async_all(): - if person.domain != "person": - continue - + for person in hass.states.async_all("person"): if "latitude" in person.attributes and "longitude" in person.attributes: response.append( { diff --git a/homeassistant/core.py b/homeassistant/core.py index ad083f60574..8f3809bbd4c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -918,17 +918,29 @@ class StateMachine: if state.domain in domain_filter ] - def all(self) -> List[State]: + def all(self, domain_filter: Optional[Union[str, Iterable]] = None) -> List[State]: """Create a list of all states.""" - return run_callback_threadsafe(self._loop, self.async_all).result() + return run_callback_threadsafe( + self._loop, self.async_all, domain_filter + ).result() @callback - def async_all(self) -> List[State]: - """Create a list of all states. + def async_all( + self, domain_filter: Optional[Union[str, Iterable]] = None + ) -> List[State]: + """Create a list of all states matching the filter. This method must be run in the event loop. """ - return list(self._states.values()) + if domain_filter is None: + return list(self._states.values()) + + if isinstance(domain_filter, str): + domain_filter = (domain_filter.lower(),) + + return [ + state for state in self._states.values() if state.domain in domain_filter + ] def get(self, entity_id: str) -> Optional[State]: """Retrieve state of entity_id or None if not found. diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index eeb43fb8756..405d8588532 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -459,8 +459,7 @@ class DomainStates: sorted( ( _wrap_state(self._hass, state) - for state in self._hass.states.async_all() - if state.domain == self._domain + for state in self._hass.states.async_all(self._domain) ), key=lambda state: state.entity_id, ) diff --git a/tests/test_core.py b/tests/test_core.py index 22f1e779061..f5de9c5f1a1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1454,3 +1454,26 @@ async def test_chained_logging_misses_log_timeout(hass, caplog): await hass.async_block_till_done() assert "_task_chain_" not in caplog.text + + +async def test_async_all(hass): + """Test async_all.""" + + hass.states.async_set("switch.link", "on") + hass.states.async_set("light.bowl", "on") + hass.states.async_set("light.frog", "on") + hass.states.async_set("vacuum.floor", "on") + + assert {state.entity_id for state in hass.states.async_all()} == { + "switch.link", + "light.bowl", + "light.frog", + "vacuum.floor", + } + assert {state.entity_id for state in hass.states.async_all("light")} == { + "light.bowl", + "light.frog", + } + assert { + state.entity_id for state in hass.states.async_all(["light", "switch"]) + } == {"light.bowl", "light.frog", "switch.link"} From 41abc08d63bd2e20542e32eb0552358c9c4cdd19 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 6 Sep 2020 23:26:06 +0200 Subject: [PATCH 695/862] Axis - Improve naming of some events (#39699) * Add support for events from Loitering guard and Motion guard * Improve naming of events originating from applications Fence guard and VMD4 (Loitering guard and motion guard also benefit from this) --- .../components/axis/binary_sensor.py | 19 +++++++++++++++ homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/axis/test_binary_sensor.py | 4 ++-- tests/components/axis/test_device.py | 23 +++++++++++++++++++ 6 files changed, 47 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index feae2c8fc99..dcdf440a472 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -8,6 +8,10 @@ from axis.event_stream import ( CLASS_MOTION, CLASS_OUTPUT, CLASS_SOUND, + FenceGuard, + LoiteringGuard, + MotionGuard, + Vmd4, ) from homeassistant.components.binary_sensor import ( @@ -104,6 +108,21 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): f"{self.device.name} {self.device.api.vapix.ports[self.event.id].name}" ) + if self.event.CLASS == CLASS_MOTION: + + for event_class, event_data in ( + (FenceGuard, self.device.api.vapix.fence_guard), + (LoiteringGuard, self.device.api.vapix.loitering_guard), + (MotionGuard, self.device.api.vapix.motion_guard), + (Vmd4, self.device.api.vapix.vmd4), + ): + if ( + isinstance(self.event, event_class) + and event_data + and self.event.id in event_data + ): + return f"{self.device.name} {self.event.TYPE} {event_data[self.event.id].name}" + return super().name @property diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 95175fce51a..3b08c5ad4d4 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==33"], + "requirements": ["axis==35"], "zeroconf": ["_axis-video._tcp.local."], "after_dependencies": ["mqtt"], "codeowners": ["@Kane610"] diff --git a/requirements_all.txt b/requirements_all.txt index 7d5dc4f3e52..aaba7f4bd57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -309,7 +309,7 @@ av==8.0.2 avri-api==0.1.7 # homeassistant.components.axis -axis==33 +axis==35 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c30f58f921..30d995ad9a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ av==8.0.2 avri-api==0.1.7 # homeassistant.components.axis -axis==33 +axis==35 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 1ffe37ef857..e2d820a959e 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -63,7 +63,7 @@ async def test_binary_sensors(hass): assert pir.name == f"{NAME} PIR 0" assert pir.attributes["device_class"] == DEVICE_CLASS_MOTION - vmd4 = hass.states.get(f"binary_sensor.{NAME}_vmd4_camera1profile1") + vmd4 = hass.states.get(f"binary_sensor.{NAME}_vmd4_profile_1") assert vmd4.state == "on" - assert vmd4.name == f"{NAME} VMD4 Camera1Profile1" + assert vmd4.name == f"{NAME} VMD4 Profile 1" assert vmd4.attributes["device_class"] == DEVICE_CLASS_MOTION diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index c6d78ca66e9..dc4cb607934 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -5,6 +5,8 @@ from unittest import mock import axis as axislib from axis.api_discovery import URL as API_DISCOVERY_URL +from axis.applications import URL_LIST as APPLICATIONS_URL +from axis.applications.vmd4 import URL as VMD4_URL from axis.basic_device_info import URL as BASIC_DEVICE_INFO_URL from axis.event_stream import OPERATION_INITIALIZED from axis.light_control import URL as LIGHT_CONTROL_URL @@ -79,6 +81,10 @@ API_DISCOVERY_PORT_MANAGEMENT = { "name": "IO Port Management", } +APPLICATIONS_LIST_RESPONSE = """ + +""" + BASIC_DEVICE_INFO_RESPONSE = { "apiVersion": "1.1", "data": { @@ -138,6 +144,18 @@ PORT_MANAGEMENT_RESPONSE = { }, } +VMD4_RESPONSE = { + "apiVersion": "1.4", + "method": "getConfiguration", + "context": "Axis library", + "data": { + "cameras": [{"id": 1, "rotation": 0, "active": True}], + "profiles": [ + {"filters": [], "camera": 1, "triggers": [], "name": "Profile 1", "uid": 1} + ], + }, +} + BRAND_RESPONSE = """root.Brand.Brand=AXIS root.Brand.ProdFullName=AXIS M1065-LW Network Camera root.Brand.ProdNbr=M1065-LW @@ -158,6 +176,7 @@ root.Output.NbrOfOutputs=0 PROPERTIES_RESPONSE = """root.Properties.API.HTTP.Version=3 root.Properties.API.Metadata.Metadata=yes root.Properties.API.Metadata.Version=1.0 +root.Properties.EmbeddedDevelopment.Version=2.16 root.Properties.Firmware.BuildDate=Feb 15 2019 09:42 root.Properties.Firmware.BuildNumber=26 root.Properties.Firmware.Version=9.10.1 @@ -182,6 +201,8 @@ def vapix_session_request(session, url, **kwargs): """Return data based on url.""" if API_DISCOVERY_URL in url: return json.dumps(API_DISCOVERY_RESPONSE) + if APPLICATIONS_URL in url: + return APPLICATIONS_LIST_RESPONSE if BASIC_DEVICE_INFO_URL in url: return json.dumps(BASIC_DEVICE_INFO_RESPONSE) if LIGHT_CONTROL_URL in url: @@ -190,6 +211,8 @@ def vapix_session_request(session, url, **kwargs): return json.dumps(MQTT_CLIENT_RESPONSE) if PORT_MANAGEMENT_URL in url: return json.dumps(PORT_MANAGEMENT_RESPONSE) + if VMD4_URL in url: + return json.dumps(VMD4_RESPONSE) if BRAND_URL in url: return BRAND_RESPONSE if IOPORT_URL in url or INPUT_URL in url or OUTPUT_URL in url: From 8eed7110a1bd41799ca964749367ee87d9f8f39c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 6 Sep 2020 23:41:41 +0200 Subject: [PATCH 696/862] Add hassfest requirements validation (#39329) --- requirements_test.txt | 3 + script/hassfest/__main__.py | 14 ++- script/hassfest/model.py | 1 + script/hassfest/requirements.py | 173 ++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 script/hassfest/requirements.py diff --git a/requirements_test.txt b/requirements_test.txt index 600916615be..86b8b496e83 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,6 +12,7 @@ mypy==0.780 pre-commit==2.7.1 pylint==2.6.0 astroid==2.4.2 +pipdeptree==1.0.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.10.0 @@ -22,3 +23,5 @@ pytest-xdist==1.32.0 pytest==5.4.3 requests_mock==1.8.0 responses==0.10.6 +stdlib-list==0.7.0 +tqdm==4.48.2 diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 6fbefc11a2f..26af118d11e 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -11,6 +11,7 @@ from . import ( dependencies, json, manifest, + requirements, services, ssdp, translations, @@ -55,6 +56,11 @@ def get_config() -> Config: type=valid_integration_path, help="Validate a single integration", ) + parser.add_argument( + "--requirements", + action="store_true", + help="Validate requirements", + ) parsed = parser.parse_args() if parsed.action is None: @@ -75,6 +81,7 @@ def get_config() -> Config: root=pathlib.Path(".").absolute(), specific_integrations=parsed.integration_path, action=parsed.action, + requirements=parsed.requirements, ) @@ -86,7 +93,10 @@ def main(): print(err) return 1 - plugins = INTEGRATION_PLUGINS + plugins = [*INTEGRATION_PLUGINS] + + if config.requirements: + plugins.append(requirements) if config.specific_integrations: integrations = {} @@ -104,6 +114,8 @@ def main(): try: start = monotonic() print(f"Validating {plugin.__name__.split('.')[-1]}...", end="", flush=True) + if plugin is requirements: + print() plugin.validate(integrations, config) print(" done in {:.2f}s".format(monotonic() - start)) except RuntimeError as err: diff --git a/script/hassfest/model.py b/script/hassfest/model.py index c993689aaab..8c55c2818f1 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -27,6 +27,7 @@ class Config: specific_integrations: Optional[pathlib.Path] = attr.ib() root: pathlib.Path = attr.ib() action: str = attr.ib() + requirements: bool = attr.ib() errors: List[Error] = attr.ib(factory=list) cache: Dict[str, Any] = attr.ib(factory=dict) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py new file mode 100644 index 00000000000..ab43cd62bd5 --- /dev/null +++ b/script/hassfest/requirements.py @@ -0,0 +1,173 @@ +"""Validate requirements.""" +import operator +import re +import subprocess +import sys +from typing import Dict, Set + +from stdlib_list import stdlib_list +from tqdm import tqdm + +from homeassistant.const import REQUIRED_PYTHON_VER +import homeassistant.util.package as pkg_util +from script.gen_requirements_all import COMMENT_REQUIREMENTS + +from .model import Config, Integration + +IGNORE_PACKAGES = { + commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS +} +PACKAGE_REGEX = re.compile(r"^(?:--.+\s)?([-_\.\w\d]+).*==.+$") +PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") +SUPPORTED_PYTHON_TUPLES = [ + REQUIRED_PYTHON_VER[:2], + tuple(map(operator.add, REQUIRED_PYTHON_VER, (0, 1, 0)))[:2], +] +SUPPORTED_PYTHON_VERSIONS = [ + ".".join(map(str, version_tuple)) for version_tuple in SUPPORTED_PYTHON_TUPLES +] +STD_LIBS = {version: set(stdlib_list(version)) for version in SUPPORTED_PYTHON_VERSIONS} + + +def normalize_package_name(requirement: str) -> str: + """Return a normalized package name from a requirement string.""" + match = PACKAGE_REGEX.search(requirement) + if not match: + return "" + + # pipdeptree needs lowercase and dash instead of underscore as separator + package = match.group(1).lower().replace("_", "-") + + return package + + +def validate(integrations: Dict[str, Integration], config: Config): + """Handle requirements for integrations.""" + # check for incompatible requirements + for integration in tqdm(integrations.values()): + if not integration.manifest: + continue + + validate_requirements(integration) + + +def validate_requirements(integration: Integration): + """Validate requirements.""" + integration_requirements = set() + integration_packages = set() + for req in integration.requirements: + package = normalize_package_name(req) + if not package: + integration.add_error( + "requirements", + f"Failed to normalize package name from requirement {req}", + ) + return + if package in IGNORE_PACKAGES: + continue + integration_requirements.add(req) + integration_packages.add(package) + + install_ok = install_requirements(integration, integration_requirements) + + if not install_ok: + return + + all_integration_requirements = get_requirements(integration, integration_packages) + + if integration_requirements and not all_integration_requirements: + integration.add_error( + "requirements", + f"Failed to resolve requirements {integration_requirements}", + ) + return + + # Check for requirements incompatible with standard library. + for version, std_libs in STD_LIBS.items(): + for req in all_integration_requirements: + if req in std_libs: + integration.add_error( + "requirements", + f"Package {req} is not compatible with Python {version} standard library", + ) + + +def get_requirements(integration: Integration, packages: Set[str]) -> Set[str]: + """Return all (recursively) requirements for an integration.""" + all_requirements = set() + + for package in packages: + try: + result = subprocess.run( + ["pipdeptree", "-w", "silence", "--packages", package], + check=True, + capture_output=True, + text=True, + ) + except subprocess.SubprocessError: + integration.add_error( + "requirements", f"Failed to resolve requirements for {package}" + ) + continue + + # parse output to get a set of package names + output = result.stdout + lines = output.split("\n") + parent = lines[0].split("==")[0] # the first line is the parent package + if parent: + all_requirements.add(parent) + + for line in lines[1:]: # skip the first line which we already processed + line = line.strip() + line = line.lstrip("- ") + package = line.split("[")[0] + package = package.strip() + if not package: + continue + all_requirements.add(package) + + return all_requirements + + +def install_requirements(integration: Integration, requirements: Set[str]) -> bool: + """Install integration requirements. + + Return True if successful. + """ + for req in requirements: + try: + is_installed = pkg_util.is_installed(req) + except ValueError: + is_installed = False + + if is_installed: + continue + + match = PIP_REGEX.search(req) + + if not match: + integration.add_error( + "requirements", + f"Failed to parse requirement {req} before installation", + ) + continue + + install_args = match.group(1) + requirement_arg = match.group(2) + + args = [sys.executable, "-m", "pip", "install", "--quiet"] + if install_args: + args.append(install_args) + args.append(requirement_arg) + try: + subprocess.run(args, check=True) + except subprocess.SubprocessError: + integration.add_error( + "requirements", + f"Requirement {req} failed to install", + ) + + if integration.errors: + return False + + return True From 165dd351b7bdfa55d56d0ee18d97d9b101ccce19 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 6 Sep 2020 15:04:55 -0700 Subject: [PATCH 697/862] Fix some missing ozw sensors (#39686) --- homeassistant/components/ozw/sensor.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ozw/sensor.py b/homeassistant/components/ozw/sensor.py index bbb13352b0b..5bd0d1c482f 100644 --- a/homeassistant/components/ozw/sensor.py +++ b/homeassistant/components/ozw/sensor.py @@ -2,7 +2,7 @@ import logging -from openzwavemqtt.const import CommandClass +from openzwavemqtt.const import CommandClass, ValueType from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, @@ -30,13 +30,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def async_add_sensor(value): """Add Z-Wave Sensor.""" # Basic Sensor types - if isinstance(value.primary.value, (float, int)): + if value.primary.type in ( + ValueType.BYTE, + ValueType.INT, + ValueType.SHORT, + ValueType.DECIMAL, + ): sensor = ZWaveNumericSensor(value) - elif isinstance(value.primary.value, dict): + elif value.primary.type == ValueType.LIST: sensor = ZWaveListSensor(value) - elif isinstance(value.primary.value, str): + elif value.primary.type == ValueType.STRING: sensor = ZWaveStringSensor(value) else: From 1ec3446c560f3bda9f8a58ec0be6c0a0611c5458 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Sep 2020 00:36:01 +0200 Subject: [PATCH 698/862] State condition can also accept an input_* Entity ID as state value (#39691) --- homeassistant/helpers/condition.py | 25 +++++++-- tests/helpers/test_condition.py | 82 ++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 473641b10a4..1b09348415c 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -4,6 +4,7 @@ from collections import deque from datetime import datetime, timedelta import functools as ft import logging +import re import sys from typing import Any, Callable, Container, List, Optional, Set, Union, cast @@ -48,6 +49,10 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" _LOGGER = logging.getLogger(__name__) +INPUT_ENTITY_ID = re.compile( + r"^input_(?:select|text|number|boolean|datetime)\.(?!.+__)(?!_)[\da-z_]+(? Date: Sun, 6 Sep 2020 18:54:24 -0400 Subject: [PATCH 699/862] allow creating directories from camera snapshot, stream record, and tensorflow file out (#39728) --- homeassistant/components/camera/__init__.py | 3 +++ homeassistant/components/stream/recorder.py | 5 ++++- homeassistant/components/tensorflow/image_processing.py | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 3de47db80d1..f6b909231ca 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ from contextlib import suppress from datetime import timedelta import hashlib import logging +import os from random import SystemRandom from aiohttp import web @@ -660,6 +661,8 @@ async def async_handle_snapshot_service(camera, service): def _write_image(to_file, image_data): """Executor helper to write image.""" + if not os.path.exists(os.path.dirname(to_file)): + os.makedirs(os.path.dirname(to_file), exist_ok=True) with open(to_file, "wb") as img_file: img_file.write(image_data) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 0a93ea0bc92..82b146cc51f 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,5 +1,5 @@ """Provide functionality to record stream.""" - +import os import threading from typing import List @@ -17,6 +17,9 @@ def async_setup_recorder(hass): def recorder_save_worker(file_out: str, segments: List[Segment], container_format: str): """Handle saving stream.""" + if not os.path.exists(os.path.dirname(file_out)): + os.makedirs(os.path.dirname(file_out), exist_ok=True) + first_pts = {"video": None, "audio": None} output = av.open(file_out, "w", format=container_format) output_v = None diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 564dd7377b1..0e73d9e871b 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -328,6 +328,8 @@ class TensorFlowImageProcessor(ImageProcessingEntity): for path in paths: _LOGGER.info("Saving results image to %s", path) + if not os.path.exists(os.path.dirname(path)): + os.makedirs(os.path.dirname(path), exist_ok=True) img.save(path) def process_image(self, image): From 4779916ac4a0bc334f103845dba0cdc4400bc1be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Sep 2020 18:55:20 -0500 Subject: [PATCH 700/862] Set DEVICE_CLASS_GATE for iSmartGate gates (#39703) --- homeassistant/components/gogogate2/cover.py | 5 +++ .../components/gogogate2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gogogate2/test_cover.py | 33 ++++++++++++++++--- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index bd7f176856b..8e753eb6ae5 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity, @@ -119,6 +120,10 @@ class DeviceCover(CoordinatorEntity, CoverEntity): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" + door = self._get_door() + if door.gate: + return DEVICE_CLASS_GATE + return DEVICE_CLASS_GARAGE @property diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index b6921991ecc..edf69144f62 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -3,7 +3,7 @@ "name": "Gogogate2 and iSmartGate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["gogogate2-api==2.0.0"], + "requirements": ["gogogate2-api==2.0.1"], "codeowners": ["@vangorra"], "homekit": { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index aaba7f4bd57..f8c452c2de4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -668,7 +668,7 @@ glances_api==0.2.0 gntp==1.0.3 # homeassistant.components.gogogate2 -gogogate2-api==2.0.0 +gogogate2-api==2.0.1 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30d995ad9a5..1b41e369779 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -334,7 +334,7 @@ gios==0.1.4 glances_api==0.2.0 # homeassistant.components.gogogate2 -gogogate2-api==2.0.0 +gogogate2-api==2.0.1 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index dd19d408741..eb2907e2b6e 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -16,7 +16,11 @@ from gogogate2_api.common import ( Wifi, ) -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.cover import ( + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, + DOMAIN as COVER_DOMAIN, +) from homeassistant.components.gogogate2.const import ( DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, @@ -26,6 +30,7 @@ from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( + ATTR_DEVICE_CLASS, CONF_DEVICE, CONF_IP_ADDRESS, CONF_NAME, @@ -97,6 +102,7 @@ async def test_import( door_id=1, permission=True, name="Door1", + gate=False, mode=DoorMode.GARAGE, status=DoorStatus.OPENED, sensor=True, @@ -109,6 +115,7 @@ async def test_import( door_id=2, permission=True, name=None, + gate=True, mode=DoorMode.GARAGE, status=DoorStatus.UNDEFINED, sensor=True, @@ -121,6 +128,7 @@ async def test_import( door_id=3, permission=True, name=None, + gate=False, mode=DoorMode.GARAGE, status=DoorStatus.UNDEFINED, sensor=True, @@ -151,6 +159,7 @@ async def test_import( door_id=1, permission=True, name="Door1", + gate=False, mode=DoorMode.GARAGE, status=DoorStatus.CLOSED, sensor=True, @@ -166,6 +175,7 @@ async def test_import( door_id=1, permission=True, name=None, + gate=True, mode=DoorMode.GARAGE, status=DoorStatus.CLOSED, sensor=True, @@ -181,6 +191,7 @@ async def test_import( door_id=1, permission=True, name=None, + gate=False, mode=DoorMode.GARAGE, status=DoorStatus.CLOSED, sensor=True, @@ -249,6 +260,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: door_id=1, permission=True, name="Door1", + gate=False, mode=DoorMode.GARAGE, status=door_status, sensor=True, @@ -261,6 +273,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: door_id=2, permission=True, name=None, + gate=True, mode=DoorMode.GARAGE, status=DoorStatus.UNDEFINED, sensor=True, @@ -273,6 +286,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: door_id=3, permission=True, name=None, + gate=False, mode=DoorMode.GARAGE, status=DoorStatus.UNDEFINED, sensor=True, @@ -356,6 +370,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: door_id=1, permission=True, name="Door1", + gate=False, mode=DoorMode.GARAGE, status=DoorStatus.CLOSED, sensor=True, @@ -370,13 +385,14 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: door2=ISmartGateDoor( door_id=2, permission=True, - name=None, + name="Door2", + gate=True, mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, + status=DoorStatus.CLOSED, sensor=True, sensorid=None, camera=False, - events=0, + events=2, temperature=None, enabled=True, apicode="apicode0", @@ -386,6 +402,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: door_id=3, permission=True, name=None, + gate=False, mode=DoorMode.GARAGE, status=DoorStatus.UNDEFINED, sensor=True, @@ -421,6 +438,14 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert hass.states.get("cover.door1") + assert ( + hass.states.get("cover.door1").attributes[ATTR_DEVICE_CLASS] + == DEVICE_CLASS_GARAGE + ) + assert ( + hass.states.get("cover.door2").attributes[ATTR_DEVICE_CLASS] + == DEVICE_CLASS_GATE + ) api.info.side_effect = Exception("Error") From c74f187b1f3f39d1cd0ee3f5c40149ddc99acd68 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 7 Sep 2020 00:03:02 +0000 Subject: [PATCH 701/862] [ci skip] Translation update --- .../components/dsmr/translations/pl.json | 7 ++++ .../gogogate2/translations/zh-Hant.json | 2 +- .../homematicip_cloud/translations/fr.json | 1 + .../components/insteon/translations/fr.json | 6 ++++ .../components/insteon/translations/pl.json | 11 +++++- .../components/kodi/translations/pl.json | 35 ++++++++++++++++++- .../nightscout/translations/fr.json | 7 ++++ .../components/nzbget/translations/pl.json | 6 +++- .../openweathermap/translations/fr.json | 29 +++++++++++++++ .../components/plugwise/translations/en.json | 10 ++++++ .../progettihwsw/translations/pl.json | 15 ++++++++ .../components/risco/translations/pl.json | 20 +++++++++++ .../components/sentry/translations/pl.json | 3 +- .../components/sharkiq/translations/pl.json | 11 ++++++ .../components/smappee/translations/fr.json | 5 +++ .../components/weather/translations/pl.json | 14 ++++---- .../components/yeelight/translations/pl.json | 16 ++++----- .../components/zha/translations/fr.json | 1 + .../components/zwave/translations/pl.json | 2 +- 19 files changed, 180 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/dsmr/translations/pl.json create mode 100644 homeassistant/components/openweathermap/translations/fr.json create mode 100644 homeassistant/components/progettihwsw/translations/pl.json create mode 100644 homeassistant/components/risco/translations/pl.json diff --git a/homeassistant/components/dsmr/translations/pl.json b/homeassistant/components/dsmr/translations/pl.json new file mode 100644 index 00000000000..815a6f19706 --- /dev/null +++ b/homeassistant/components/dsmr/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/zh-Hant.json b/homeassistant/components/gogogate2/translations/zh-Hant.json index 7ba01116084..607794131ef 100644 --- a/homeassistant/components/gogogate2/translations/zh-Hant.json +++ b/homeassistant/components/gogogate2/translations/zh-Hant.json @@ -15,7 +15,7 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8acb\u65bc\u4e0b\u65b9\u63d0\u4f9b\u6240\u9700\u8cc7\u8a0a\u3002", - "title": "\u8a2d\u5b9a GogoGate2" + "title": "\u8a2d\u5b9a GogoGate2 \u6216 iSmartGate" } } } diff --git a/homeassistant/components/homematicip_cloud/translations/fr.json b/homeassistant/components/homematicip_cloud/translations/fr.json index 9bcb8be7e0e..585334b3118 100644 --- a/homeassistant/components/homematicip_cloud/translations/fr.json +++ b/homeassistant/components/homematicip_cloud/translations/fr.json @@ -6,6 +6,7 @@ "unknown": "Une erreur inconnue s'est produite." }, "error": { + "invalid_pin": "Code PIN invalide, veuillez r\u00e9essayer.", "invalid_sgtin_or_pin": "Code PIN invalide, veuillez r\u00e9essayer.", "press_the_button": "Veuillez appuyer sur le bouton bleu.", "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer.", diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index f44be025271..e0f56f093af 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -67,12 +67,18 @@ } }, "options": { + "abort": { + "cannot_connect": "Impossible de se connecter au modem Insteon" + }, "error": { "cannot_connect": "\u00c9chec de connexion", "select_single": "S\u00e9lectionnez une option" }, "step": { "add_override": { + "data": { + "cat": "Cat\u00e9gorie d'appareil (c.-\u00e0-d. 0x10)" + }, "title": "Insteon" }, "add_x10": { diff --git a/homeassistant/components/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json index 1fe0f6b7451..cb697462ca9 100644 --- a/homeassistant/components/insteon/translations/pl.json +++ b/homeassistant/components/insteon/translations/pl.json @@ -1,15 +1,24 @@ { "config": { "abort": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "step": { + "hubv1": { + "data": { + "host": "Adres IP", + "port": "Port" + } + }, "hubv2": { "data": { "host": "Adres IP", + "password": "Has\u0142o", + "port": "Port", "username": "Nazwa u\u017cytkownika" } }, diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json index d9590796ce5..3e71fd0df8b 100644 --- a/homeassistant/components/kodi/translations/pl.json +++ b/homeassistant/components/kodi/translations/pl.json @@ -1,7 +1,40 @@ { "config": { "abort": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, + "step": { + "credentials": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, + "host": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + } + }, + "ws_port": { + "data": { + "ws_port": "Port" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json index c4bc0d48b1a..5b31b06ea3a 100644 --- a/homeassistant/components/nightscout/translations/fr.json +++ b/homeassistant/components/nightscout/translations/fr.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/pl.json b/homeassistant/components/nzbget/translations/pl.json index 121f77b0a96..a5bd1b5cdcb 100644 --- a/homeassistant/components/nzbget/translations/pl.json +++ b/homeassistant/components/nzbget/translations/pl.json @@ -7,7 +7,11 @@ "step": { "user": { "data": { - "name": "Nazwa" + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" } } } diff --git a/homeassistant/components/openweathermap/translations/fr.json b/homeassistant/components/openweathermap/translations/fr.json new file mode 100644 index 00000000000..b55997e1f8d --- /dev/null +++ b/homeassistant/components/openweathermap/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "auth": "La cl\u00e9 API n'est pas correcte." + }, + "step": { + "user": { + "data": { + "language": "Langue", + "latitude": "Latitude", + "longitude": "Longitude", + "mode": "Mode", + "name": "Nom de l'int\u00e9gration" + }, + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "Langue", + "mode": "Mode" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 13f026c2eb5..238f435f3ab 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -19,5 +19,15 @@ "title": "Connect to the Smile" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Scan Interval (seconds)" + }, + "description": "Adjust Plugwise Options" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/pl.json b/homeassistant/components/progettihwsw/translations/pl.json new file mode 100644 index 00000000000..ee25c598ecd --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json new file mode 100644 index 00000000000..9859923d31e --- /dev/null +++ b/homeassistant/components/risco/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/pl.json b/homeassistant/components/sentry/translations/pl.json index a66e217b850..fa87b8510c7 100644 --- a/homeassistant/components/sentry/translations/pl.json +++ b/homeassistant/components/sentry/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Sentry jest ju\u017c skonfigurowane." + "already_configured": "Sentry jest ju\u017c skonfigurowane.", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { "bad_dsn": "Nieprawid\u0142owy DSN", diff --git a/homeassistant/components/sharkiq/translations/pl.json b/homeassistant/components/sharkiq/translations/pl.json index c32bd13fe16..dcb12e86906 100644 --- a/homeassistant/components/sharkiq/translations/pl.json +++ b/homeassistant/components/sharkiq/translations/pl.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "reauth_successful": "Token dost\u0119pu zosta\u0142 pomy\u015blnie zaktualizowany", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie." }, @@ -9,6 +14,12 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" } + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } } } } diff --git a/homeassistant/components/smappee/translations/fr.json b/homeassistant/components/smappee/translations/fr.json index f231f1f1371..18985ce9f34 100644 --- a/homeassistant/components/smappee/translations/fr.json +++ b/homeassistant/components/smappee/translations/fr.json @@ -5,6 +5,11 @@ "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "step": { + "environment": { + "data": { + "environment": "Environnement" + } + }, "pick_implementation": { "title": "Choisissez la m\u00e9thode d'authentification" } diff --git a/homeassistant/components/weather/translations/pl.json b/homeassistant/components/weather/translations/pl.json index 8dbd5bd3b3a..dcd70a3d0e3 100644 --- a/homeassistant/components/weather/translations/pl.json +++ b/homeassistant/components/weather/translations/pl.json @@ -1,14 +1,14 @@ { "state": { "_": { - "clear-night": "pogodnie, noc", - "cloudy": "pochmurno", + "clear-night": "Pogodna noc", + "cloudy": "Pochmurno", "exceptional": "wyj\u0105tkowy", - "fog": "mg\u0142a", - "hail": "grad", - "lightning": "b\u0142yskawice", - "lightning-rainy": "burza", - "partlycloudy": "cz\u0119\u015bciowe zachmurzenie", + "fog": "Mg\u0142a", + "hail": "Grad", + "lightning": "B\u0142yskawice", + "lightning-rainy": "Burza", + "partlycloudy": "Cz\u0119\u015bciowe zachmurzenie", "pouring": "Ulewa", "rainy": "Deszczowo", "snowy": "\u015anie\u017cnie", diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json index 7fd730d5248..4a2636457aa 100644 --- a/homeassistant/components/yeelight/translations/pl.json +++ b/homeassistant/components/yeelight/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci." }, "error": { - "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "step": { "pick_device": { @@ -15,10 +15,10 @@ }, "user": { "data": { - "host": "Host", + "host": "Nazwa hosta lub adres IP", "ip_address": "Adres IP" }, - "description": "Je\u015bli nie podasz hosta, to wykrywanie zostanie u\u017cyte do odnalezienia urz\u0105dze\u0144." + "description": "Je\u015bli nie podasz IP lub nazwy hosta, wykrywanie zostanie u\u017cyte do odnalezienia urz\u0105dze\u0144." } } }, @@ -26,11 +26,11 @@ "step": { "init": { "data": { - "model": "Model (Opcjonalne)", - "nightlight_switch": "U\u017cyj prze\u0142\u0105cznika Nocnego wiat\u0142a", + "model": "Model (opcjonalne)", + "nightlight_switch": "U\u017cyj prze\u0142\u0105cznika Nocnego \u015bwiat\u0142a", "save_on_change": "Zachowaj status po zmianie", "transition": "Czas przej\u015bcia (ms)", - "use_music_mode": "Aktywuj tryb muzyczny" + "use_music_mode": "W\u0142\u0105cz tryb muzyczny" }, "description": "Je\u015bli nie podasz modelu urz\u0105dzenia, zostanie on automatycznie wykryty." } diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 11e58f7be31..1bfe4e5a3ac 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -65,6 +65,7 @@ "device_dropped": "Appareil tomb\u00e9", "device_flipped": "Appareil retourn\u00e9 \"{subtype}\"", "device_knocked": "Appareil frapp\u00e9 \"{subtype}\"", + "device_offline": "Appareil hors ligne", "device_rotated": "Appareil tourn\u00e9 \"{subtype}\"", "device_shaken": "Appareil secou\u00e9", "device_slid": "Appareil gliss\u00e9 \"{subtype}\"", diff --git a/homeassistant/components/zwave/translations/pl.json b/homeassistant/components/zwave/translations/pl.json index 17ab30c1898..ecff84ceb97 100644 --- a/homeassistant/components/zwave/translations/pl.json +++ b/homeassistant/components/zwave/translations/pl.json @@ -23,7 +23,7 @@ "dead": "martwy", "initializing": "Inicjowanie", "ready": "Gotowe", - "sleeping": "u\u015bpiony" + "sleeping": "U\u015bpiony" }, "query_stage": { "dead": "martwy", From d0af3339fc33b197ac36f7d2e700a931db974ccb Mon Sep 17 00:00:00 2001 From: Andrew Marks Date: Sun, 6 Sep 2020 21:29:17 -0400 Subject: [PATCH 702/862] Add unique_id to jewish_calendar entities (#39025) --- .../components/jewish_calendar/__init__.py | 26 +++++++++++++++++ .../jewish_calendar/binary_sensor.py | 6 ++++ .../components/jewish_calendar/sensor.py | 6 ++++ tests/components/jewish_calendar/__init__.py | 1 + .../jewish_calendar/test_binary_sensor.py | 27 ++++++++++++++++- .../components/jewish_calendar/test_sensor.py | 29 ++++++++++++++++++- 6 files changed, 93 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 45f979874f7..dfde274faa8 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -1,5 +1,6 @@ """The jewish_calendar component.""" import logging +from typing import Optional import hdate import voluptuous as vol @@ -77,6 +78,27 @@ CONFIG_SCHEMA = vol.Schema( ) +def get_unique_prefix( + location: hdate.Location, + language: str, + candle_lighting_offset: Optional[int], + havdalah_offset: Optional[int], +) -> str: + """Create a prefix for unique ids.""" + config_properties = [ + location.latitude, + location.longitude, + location.timezone, + location.altitude, + location.diaspora, + language, + candle_lighting_offset, + havdalah_offset, + ] + prefix = "_".join(map(str, config_properties)) + return f"{prefix}" + + async def async_setup(hass, config): """Set up the Jewish Calendar component.""" name = config[DOMAIN][CONF_NAME] @@ -96,6 +118,9 @@ async def async_setup(hass, config): diaspora=diaspora, ) + prefix = get_unique_prefix( + location, language, candle_lighting_offset, havdalah_offset + ) hass.data[DOMAIN] = { "location": location, "name": name, @@ -103,6 +128,7 @@ async def async_setup(hass, config): "candle_lighting_offset": candle_lighting_offset, "havdalah_offset": havdalah_offset, "diaspora": diaspora, + "prefix": prefix, } hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index 22e6a46e0ec..d736be0d578 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -37,12 +37,18 @@ class JewishCalendarBinarySensor(BinarySensorEntity): self._candle_lighting_offset = data["candle_lighting_offset"] self._havdalah_offset = data["havdalah_offset"] self._state = False + self._prefix = data["prefix"] @property def icon(self): """Return the icon of the entity.""" return self._icon + @property + def unique_id(self) -> str: + """Generate a unique id.""" + return f"{self._prefix}_{self._type}" + @property def name(self): """Return the name of the entity.""" diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 7da9d7e31d0..606f4fffab1 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -44,6 +44,7 @@ class JewishCalendarSensor(Entity): self._havdalah_offset = data["havdalah_offset"] self._diaspora = data["diaspora"] self._state = None + self._prefix = data["prefix"] self._holiday_attrs = {} @property @@ -51,6 +52,11 @@ class JewishCalendarSensor(Entity): """Return the name of the sensor.""" return self._name + @property + def unique_id(self) -> str: + """Generate a unique id.""" + return f"{self._prefix}_{self._type}" + @property def icon(self): """Icon to display in the front end.""" diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index 8e18579b197..c5fc2683923 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -10,6 +10,7 @@ from tests.async_mock import patch _LatLng = namedtuple("_LatLng", ["lat", "lng"]) +HDATE_DEFAULT_ALTITUDE = 754 NYC_LATLNG = _LatLng(40.7128, -74.0060) JERUSALEM_LATLNG = _LatLng(31.778, 35.235) diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index b9b980d29c2..8986c2e6f53 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -8,7 +8,12 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import alter_time, make_jerusalem_test_params, make_nyc_test_params +from . import ( + HDATE_DEFAULT_ALTITUDE, + alter_time, + make_jerusalem_test_params, + make_nyc_test_params, +) from tests.common import async_fire_time_changed @@ -79,6 +84,8 @@ async def test_issur_melacha_sensor( hass.config.latitude = latitude hass.config.longitude = longitude + registry = await hass.helpers.entity_registry.async_get_registry() + with alter_time(test_time): assert await async_setup_component( hass, @@ -103,3 +110,21 @@ async def test_issur_melacha_sensor( hass.states.get("binary_sensor.test_issur_melacha_in_effect").state == result ) + entity = registry.async_get("binary_sensor.test_issur_melacha_in_effect") + target_uid = "_".join( + map( + str, + [ + latitude, + longitude, + time_zone, + HDATE_DEFAULT_ALTITUDE, + diaspora, + "english", + candle_lighting, + havdalah, + "issur_melacha_in_effect", + ], + ) + ) + assert entity.unique_id == target_uid diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 60def2e09d2..1c771c71a3a 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -7,7 +7,12 @@ from homeassistant.components import jewish_calendar from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import alter_time, make_jerusalem_test_params, make_nyc_test_params +from . import ( + HDATE_DEFAULT_ALTITUDE, + alter_time, + make_jerusalem_test_params, + make_nyc_test_params, +) from tests.common import async_fire_time_changed @@ -506,6 +511,8 @@ async def test_shabbat_times_sensor( hass.config.latitude = latitude hass.config.longitude = longitude + registry = await hass.helpers.entity_registry.async_get_registry() + with alter_time(test_time): assert await async_setup_component( hass, @@ -543,6 +550,26 @@ async def test_shabbat_times_sensor( result_value ), f"Value for {sensor_type}" + entity = registry.async_get(f"sensor.test_{sensor_type}") + target_sensor_type = sensor_type.replace("parshat_hashavua", "weekly_portion") + target_uid = "_".join( + map( + str, + [ + latitude, + longitude, + time_zone, + HDATE_DEFAULT_ALTITUDE, + diaspora, + language, + candle_lighting, + havdalah, + target_sensor_type, + ], + ) + ) + assert entity.unique_id == target_uid + OMER_PARAMS = [ (dt(2019, 4, 21, 0), "1"), From 3a36a789ad0389bab3ade027c6b518faea57fe76 Mon Sep 17 00:00:00 2001 From: pbalogh77 Date: Mon, 7 Sep 2020 04:03:03 +0200 Subject: [PATCH 703/862] Improve climate support for fibaro (#39038) * Fibaro climate improvements 1, Implemented support for multinode climate devices, such as Danfoss HC10, differentiating zones based on endPointId 2, Improved recognition of temperature sensor subdevice for climate devices 3, Changed default opmode for devices without an opmode to "auto" instead of "fan_only", for better clarity and to avoid misunderstandings * pylint inspired code restructuring to reduce depth gotta love pylint --- homeassistant/components/fibaro/__init__.py | 73 +++++++++++++++------ homeassistant/components/fibaro/climate.py | 24 +++++-- 2 files changed, 72 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 54dd5b6234f..4b51c82469e 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -200,9 +200,26 @@ class FibaroController: if device.parentId == device_id ] - def get_siblings(self, device_id): + def get_children2(self, device_id, endpoint_id): + """Get a list of child devices for the same endpoint.""" + return [ + device + for device in self._device_map.values() + if device.parentId == device_id + and ( + "endPointId" not in device.properties + or device.properties.endPointId == endpoint_id + ) + ] + + def get_siblings(self, device): """Get the siblings of a device.""" - return self.get_children(self._device_map[device_id].parentId) + if "endPointId" in device.properties: + return self.get_children2( + self._device_map[device.id].parentId, + self._device_map[device.id].properties.endPointId, + ) + return self.get_children(self._device_map[device.id].parentId) @staticmethod def _map_device_to_type(device): @@ -262,6 +279,7 @@ class FibaroController: self._device_map = {} self.fibaro_devices = defaultdict(list) last_climate_parent = None + last_endpoint = None for device in devices: try: if "name" not in device or "id" not in device: @@ -289,23 +307,10 @@ class FibaroController: else: device.mapped_type = None dtype = device.mapped_type - if dtype: - device.unique_id_str = f"{self.hub_serial}.{device.id}" - self._device_map[device.id] = device - if dtype != "climate": - self.fibaro_devices[dtype].append(device) - else: - # if a sibling of this has been added, skip this one - # otherwise add the first visible device in the group - # which is a hack, but solves a problem with FGT having - # hidden compatibility devices before the real device - if ( - last_climate_parent != device.parentId - and "visible" in device - and device.visible - ): - self.fibaro_devices[dtype].append(device) - last_climate_parent = device.parentId + if dtype is None: + continue + device.unique_id_str = f"{self.hub_serial}.{device.id}" + self._device_map[device.id] = device _LOGGER.debug( "%s (%s, %s) -> %s %s", device.ha_id, @@ -314,6 +319,36 @@ class FibaroController: dtype, str(device), ) + if dtype != "climate": + self.fibaro_devices[dtype].append(device) + continue + # We group climate devices into groups with the same + # endPointID belonging to the same parent device. + if "endPointId" in device.properties: + _LOGGER.debug( + "climate device: %s, endPointId: %s", + device.ha_id, + device.properties.endPointId, + ) + else: + _LOGGER.debug("climate device: %s, no endPointId", device.ha_id) + # If a sibling of this device has been added, skip this one + # otherwise add the first visible device in the group + # which is a hack, but solves a problem with FGT having + # hidden compatibility devices before the real device + if last_climate_parent != device.parentId or ( + "endPointId" in device.properties + and last_endpoint != device.properties.endPointId + ): + _LOGGER.debug("Handle separately") + self.fibaro_devices[dtype].append(device) + last_climate_parent = device.parentId + if "endPointId" in device.properties: + last_endpoint = device.properties.endPointId + else: + last_endpoint = 0 + else: + _LOGGER.debug("not handling separately") except (KeyError, ValueError): pass diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 5776a293756..eee1c08bd36 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -120,12 +120,24 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._preset_support = [] self._fan_support = [] - siblings = fibaro_device.fibaro_controller.get_siblings(fibaro_device.id) + siblings = fibaro_device.fibaro_controller.get_siblings(fibaro_device) + _LOGGER.debug("%s siblings: %s", fibaro_device.ha_id, siblings) tempunit = "C" for device in siblings: + # Detecting temperature device, one strong and one weak way of + # doing so, so we prefer the hard evidence, if there is such. if device.type == "com.fibaro.temperatureSensor": self._temp_sensor_device = FibaroDevice(device) tempunit = device.properties.unit + elif ( + self._temp_sensor_device is None + and "unit" in device.properties + and "value" in device.properties + and (device.properties.unit == "C" or device.properties.unit == "F") + ): + self._temp_sensor_device = FibaroDevice(device) + tempunit = device.properties.unit + if ( "setTargetLevel" in device.actions or "setThermostatSetpoint" in device.actions @@ -133,9 +145,11 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): self._target_temp_device = FibaroDevice(device) self._support_flags |= SUPPORT_TARGET_TEMPERATURE tempunit = device.properties.unit + if "setMode" in device.actions or "setOperatingMode" in device.actions: self._op_mode_device = FibaroDevice(device) self._support_flags |= SUPPORT_PRESET_MODE + if "setFanMode" in device.actions: self._fan_mode_device = FibaroDevice(device) self._support_flags |= SUPPORT_FAN_MODE @@ -188,9 +202,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): await super().async_added_to_hass() # Register update callback for child devices - siblings = self.fibaro_device.fibaro_controller.get_siblings( - self.fibaro_device.id - ) + siblings = self.fibaro_device.fibaro_controller.get_siblings(self.fibaro_device) for device in siblings: if device != self.fibaro_device: self.controller.register(device.id, self._update_callback) @@ -225,7 +237,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): def fibaro_op_mode(self): """Return the operating mode of the device.""" if not self._op_mode_device: - return 6 # Fan only + return 3 # Default to AUTO if "operatingMode" in self._op_mode_device.fibaro_device.properties: return int(self._op_mode_device.fibaro_device.properties.operatingMode) @@ -241,7 +253,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity): def hvac_modes(self): """Return the list of available operation modes.""" if not self._op_mode_device: - return [HVAC_MODE_FAN_ONLY] + return [HVAC_MODE_AUTO] # Default to this return self._hvac_support def set_hvac_mode(self, hvac_mode): From 863b63d75e5c312cd3eb6744776f835b8401062c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Sep 2020 04:08:50 +0200 Subject: [PATCH 704/862] Fix handling of device registry defaults (#39688) --- homeassistant/helpers/device_registry.py | 13 +++-- tests/helpers/test_device_registry.py | 61 +++++++++++++++++++++++- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 7c990aa397d..1dff2ef9483 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -239,15 +239,14 @@ class DeviceRegistry: device = deleted_device.to_device_entry() self._add_device(device) - else: - if default_manufacturer and not device.manufacturer: - manufacturer = default_manufacturer + if default_manufacturer is not _UNDEF and device.manufacturer is None: + manufacturer = default_manufacturer - if default_model and not device.model: - model = default_model + if default_model is not _UNDEF and device.model is None: + model = default_model - if default_name and not device.name: - name = default_name + if default_name is not _UNDEF and device.name is None: + name = default_name if via_device is not None: via = self.async_get_device({via_device}, set()) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 5b671dce134..7c9e8a6e262 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -841,8 +841,8 @@ async def test_restore_simple_device(hass, registry, update_events): assert update_events[3]["device_id"] == entry3.id -async def test_get_or_create_sets_default_values(hass, registry): - """Make sure we do not duplicate entries.""" +async def test_get_or_create_empty_then_set_default_values(hass, registry): + """Test creating an entry, then setting default name, model, manufacturer.""" entry = registry.async_get_or_create( identifiers={("bridgeid", "0123")}, config_entry_id="1234" ) @@ -871,3 +871,60 @@ async def test_get_or_create_sets_default_values(hass, registry): assert entry.name == "default name 1" assert entry.model == "default model 1" assert entry.manufacturer == "default manufacturer 1" + + +async def test_get_or_create_empty_then_update(hass, registry): + """Test creating an entry, then setting name, model, manufacturer.""" + entry = registry.async_get_or_create( + identifiers={("bridgeid", "0123")}, config_entry_id="1234" + ) + assert entry.name is None + assert entry.model is None + assert entry.manufacturer is None + + entry = registry.async_get_or_create( + config_entry_id="1234", + identifiers={("bridgeid", "0123")}, + name="name 1", + model="model 1", + manufacturer="manufacturer 1", + ) + assert entry.name == "name 1" + assert entry.model == "model 1" + assert entry.manufacturer == "manufacturer 1" + + entry = registry.async_get_or_create( + config_entry_id="1234", + identifiers={("bridgeid", "0123")}, + default_name="default name 1", + default_model="default model 1", + default_manufacturer="default manufacturer 1", + ) + assert entry.name == "name 1" + assert entry.model == "model 1" + assert entry.manufacturer == "manufacturer 1" + + +async def test_get_or_create_sets_default_values(hass, registry): + """Test creating an entry, then setting default name, model, manufacturer.""" + entry = registry.async_get_or_create( + config_entry_id="1234", + identifiers={("bridgeid", "0123")}, + default_name="default name 1", + default_model="default model 1", + default_manufacturer="default manufacturer 1", + ) + assert entry.name == "default name 1" + assert entry.model == "default model 1" + assert entry.manufacturer == "default manufacturer 1" + + entry = registry.async_get_or_create( + config_entry_id="1234", + identifiers={("bridgeid", "0123")}, + default_name="default name 2", + default_model="default model 2", + default_manufacturer="default manufacturer 2", + ) + assert entry.name == "default name 1" + assert entry.model == "default model 1" + assert entry.manufacturer == "default manufacturer 1" From cd0195a27ac9b9e4934e0317c1b270fcdb59b97a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 6 Sep 2020 23:10:15 -0400 Subject: [PATCH 705/862] Update ZHA dependencies (#39700) * Update ZHA dependencies * Update zigpy-zigate * Move ZNP on top of the radios so it's probed 1st Some stick don't like if there was some unexpected traffic on the port prior the initialization. * Update dependencies --- homeassistant/components/zha/core/const.py | 7 ++++++- homeassistant/components/zha/manifest.json | 13 +++++++------ requirements_all.txt | 15 +++++++++------ requirements_test_all.txt | 15 +++++++++------ 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 63652f58f30..402c1505415 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -9,6 +9,7 @@ import zigpy_cc.zigbee.application import zigpy_deconz.zigbee.application import zigpy_xbee.zigbee.application import zigpy_zigate.zigbee.application +import zigpy_znp.zigbee.application from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.climate import DOMAIN as CLIMATE @@ -169,6 +170,10 @@ POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown" class RadioType(enum.Enum): """Possible options for radio type.""" + znp = ( + "ZNP = Texas Instruments Z-Stack ZNP protocol: CC253x, CC26x2, CC13x2", + zigpy_znp.zigbee.application.ControllerApplication, + ) ezsp = ( "EZSP = Silicon Labs EmberZNet protocol: Elelabs, HUSBZB-1, Telegesis", bellows.zigbee.application.ControllerApplication, @@ -178,7 +183,7 @@ class RadioType(enum.Enum): zigpy_deconz.zigbee.application.ControllerApplication, ) ti_cc = ( - "TI_CC = Texas Instruments Z-Stack ZNP protocol: CC253x, CC26x2, CC13x2", + "Legacy TI_CC = Texas Instruments Z-Stack ZNP protocol: CC253x, CC26x2, CC13x2", zigpy_cc.zigbee.application.ControllerApplication, ) zigate = ( diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index bb5da313a56..1fd8bb71920 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,14 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.18.1", + "bellows==0.20.1", "pyserial==3.4", - "zha-quirks==0.0.43", - "zigpy-cc==0.5.1", + "zha-quirks==0.0.44", + "zigpy-cc==0.5.2", "zigpy-deconz==0.9.2", - "zigpy==0.22.2", - "zigpy-xbee==0.12.1", - "zigpy-zigate==0.6.1" + "zigpy==0.23.1", + "zigpy-xbee==0.13.0", + "zigpy-zigate==0.6.2", + "zigpy-znp==0.1.1" ], "codeowners": ["@dmulcahey", "@adminiuga"] } diff --git a/requirements_all.txt b/requirements_all.txt index f8c452c2de4..31cb792e01a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ beautifulsoup4==4.9.1 # beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows==0.18.1 +bellows==0.20.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.7 @@ -2286,7 +2286,7 @@ zengge==0.2 zeroconf==0.28.4 # homeassistant.components.zha -zha-quirks==0.0.43 +zha-quirks==0.0.44 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2295,19 +2295,22 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-cc==0.5.1 +zigpy-cc==0.5.2 # homeassistant.components.zha zigpy-deconz==0.9.2 # homeassistant.components.zha -zigpy-xbee==0.12.1 +zigpy-xbee==0.13.0 # homeassistant.components.zha -zigpy-zigate==0.6.1 +zigpy-zigate==0.6.2 # homeassistant.components.zha -zigpy==0.22.2 +zigpy-znp==0.1.1 + +# homeassistant.components.zha +zigpy==0.23.1 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b41e369779..a403117580a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ azure-eventhub==5.1.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.18.1 +bellows==0.20.1 # homeassistant.components.blebox blebox_uniapi==1.3.2 @@ -1056,19 +1056,22 @@ yeelight==0.5.3 zeroconf==0.28.4 # homeassistant.components.zha -zha-quirks==0.0.43 +zha-quirks==0.0.44 # homeassistant.components.zha -zigpy-cc==0.5.1 +zigpy-cc==0.5.2 # homeassistant.components.zha zigpy-deconz==0.9.2 # homeassistant.components.zha -zigpy-xbee==0.12.1 +zigpy-xbee==0.13.0 # homeassistant.components.zha -zigpy-zigate==0.6.1 +zigpy-zigate==0.6.2 # homeassistant.components.zha -zigpy==0.22.2 +zigpy-znp==0.1.1 + +# homeassistant.components.zha +zigpy==0.23.1 From 12cc158b64f197d15549cfbd84f65dbbb6e1baff Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 6 Sep 2020 23:39:23 -0700 Subject: [PATCH 706/862] Remove ozw lock service for loops, replace with get_value() (#39735) --- homeassistant/components/ozw/lock.py | 44 +++++++++++----------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/ozw/lock.py b/homeassistant/components/ozw/lock.py index e4410adac95..3797734dfeb 100644 --- a/homeassistant/components/ozw/lock.py +++ b/homeassistant/components/ozw/lock.py @@ -1,7 +1,7 @@ """Representation of Z-Wave locks.""" import logging -from openzwavemqtt.const import CommandClass +from openzwavemqtt.const import CommandClass, ValueIndex import voluptuous as vol from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity @@ -73,35 +73,25 @@ class ZWaveLock(ZWaveDeviceEntity, LockEntity): @callback def async_set_usercode(self, code_slot, usercode): """Set the usercode to index X on the lock.""" - lock_node = self.values.primary.node.values() + value = self.values.primary.node.get_value(CommandClass.USER_CODE, code_slot) - for value in lock_node: - if ( - value.command_class == CommandClass.USER_CODE - and value.index == code_slot - ): - if len(str(usercode)) < 4: - _LOGGER.error( - "Invalid code provided: (%s) user code must be at least 4 digits", - usercode, - ) - break - value.send_value(usercode) - _LOGGER.debug("User code at slot %s set", code_slot) - break + if len(str(usercode)) < 4: + _LOGGER.error( + "Invalid code provided: (%s) user code must be at least 4 digits", + usercode, + ) + return + value.send_value(usercode) + _LOGGER.debug("User code at slot %s set", code_slot) @callback def async_clear_usercode(self, code_slot): """Clear usercode in slot X on the lock.""" - lock_node = self.values.primary.node.values() + value = self.values.primary.node.get_value( + CommandClass.USER_CODE, ValueIndex.CLEAR_USER_CODE + ) - for value in lock_node: - if ( - value.command_class == CommandClass.USER_CODE - and value.label == "Remove User Code" - ): - value.send_value(code_slot) - # Sending twice because the first time it doesn't take - value.send_value(code_slot) - _LOGGER.info("Usercode at slot %s is cleared", code_slot) - break + value.send_value(code_slot) + # Sending twice because the first time it doesn't take + value.send_value(code_slot) + _LOGGER.info("Usercode at slot %s is cleared", code_slot) From 2e6cd4f12b42fb5637d815f38ddf69dfbf16891f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2020 02:17:41 -0500 Subject: [PATCH 707/862] Optimize template sandbox for Home Assistant (#39731) --- homeassistant/helpers/template.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 405d8588532..ac326711581 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1122,7 +1122,13 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): def is_safe_attribute(self, obj, attr, value): """Test if attribute is safe.""" - return isinstance(obj, Namespace) or super().is_safe_attribute(obj, attr, value) + if isinstance(obj, Namespace): + return True + + if isinstance(obj, (AllStates, DomainStates, TemplateState)): + return not attr.startswith("_") + + return super().is_safe_attribute(obj, attr, value) def compile(self, source, name=None, filename=None, raw=False, defer_init=False): """Compile the template.""" From 2bd2dcb50aca9950ad4da1cfd460ff28353f216b Mon Sep 17 00:00:00 2001 From: RogerSelwyn Date: Mon, 7 Sep 2020 09:36:37 +0100 Subject: [PATCH 708/862] Bump pyskyqhub to 0.1.3 (#39739) To fix issue - https://github.com/RogerSelwyn/skyq_hub/issues/4 - where tests (and manage) modules includes in pypi package --- homeassistant/components/sky_hub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sky_hub/manifest.json b/homeassistant/components/sky_hub/manifest.json index a0ef4bc8e0c..965c2af5159 100644 --- a/homeassistant/components/sky_hub/manifest.json +++ b/homeassistant/components/sky_hub/manifest.json @@ -2,6 +2,6 @@ "domain": "sky_hub", "name": "Sky Hub", "documentation": "https://www.home-assistant.io/integrations/sky_hub", - "requirements": ["pyskyqhub==0.1.2"], + "requirements": ["pyskyqhub==0.1.3"], "codeowners": ["@rogerselwyn"] } diff --git a/requirements_all.txt b/requirements_all.txt index 31cb792e01a..d913c171ee9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1622,7 +1622,7 @@ pysher==1.0.1 pysignalclirestapi==0.3.4 # homeassistant.components.sky_hub -pyskyqhub==0.1.2 +pyskyqhub==0.1.3 # homeassistant.components.sma pysma==0.3.5 From 72300a54df9ab8ee271efc66008bd2712f372cae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Sep 2020 11:17:03 +0200 Subject: [PATCH 709/862] Upgrade isort to 5.5.1 (#39737) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f0c4c918ca..91add974b8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,7 +39,7 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.5.0 + rev: 5.5.1 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index f5a697982bc..2daad6e33f0 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -5,7 +5,7 @@ black==20.8b1 codespell==1.17.1 flake8-docstrings==1.5.0 flake8==3.8.3 -isort==5.5.0 +isort==5.5.1 pydocstyle==5.1.1 pyupgrade==2.7.2 yamllint==1.24.2 From 21dbce1554fb3d16346a2406d69ff08a14f88fad Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 7 Sep 2020 11:24:31 +0200 Subject: [PATCH 710/862] Make Sonos use BrowseMedia (#39742) --- .../components/sonos/media_player.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 3da46b07e9e..47610f242eb 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1479,14 +1479,14 @@ def item_payload(item): Used by async_browse_media. """ - return { - "title": item.title, - "thumbnail": getattr(item, "album_art_uri", None), - "media_content_id": get_content_id(item), - "media_content_type": SONOS_TO_MEDIA_TYPES[get_media_type(item)], - "can_play": can_play(item.item_class), - "can_expand": can_expand(item), - } + return BrowseMedia( + title=item.title, + thumbnail=getattr(item, "album_art_uri", None), + media_content_id=get_content_id(item), + media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)], + can_play=can_play(item.item_class), + can_expand=can_expand(item), + ) def library_payload(media_library): @@ -1495,14 +1495,14 @@ def library_payload(media_library): Used by async_browse_media. """ - return { - "title": "Music Library", - "media_content_id": "library", - "media_content_type": "library", - "can_play": False, - "can_expand": True, - "children": [item_payload(item) for item in media_library.browse()], - } + return BrowseMedia( + title="Music Library", + media_content_id="library", + media_content_type="library", + can_play=False, + can_expand=True, + children=[item_payload(item) for item in media_library.browse()], + ) def get_media_type(item): From 5b5b57b81007b3bdb1b079a37726a3c42950a6f5 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Sep 2020 13:17:22 +0200 Subject: [PATCH 711/862] Bump Plugwise_Smile from 1.1.0 to 1.4.0 (#39726) * Provisional commit to test latest pre-release of module * Bump version to release --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index d485fa22607..f4cb9164e5d 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["Plugwise_Smile==1.1.0"], + "requirements": ["Plugwise_Smile==1.4.0"], "codeowners": ["@CoMPaTech", "@bouwew"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index d913c171ee9..b74db09ad41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -26,7 +26,7 @@ Mastodon.py==1.5.1 OPi.GPIO==0.4.0 # homeassistant.components.plugwise -Plugwise_Smile==1.1.0 +Plugwise_Smile==1.4.0 # homeassistant.components.essent PyEssent==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a403117580a..d09fe2d07ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ HAP-python==3.0.0 # homeassistant.components.plugwise -Plugwise_Smile==1.1.0 +Plugwise_Smile==1.4.0 # homeassistant.components.flick_electric PyFlick==0.0.2 From 90c6e1c449187af8ae632e19d4740753065346d6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Sep 2020 13:44:10 +0200 Subject: [PATCH 712/862] Fix cast media player browser (#39745) --- homeassistant/components/cast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 342d6f1bee5..177babdb476 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -510,7 +510,7 @@ class CastDevice(MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" result = await media_source.async_browse_media(self.hass, media_content_id) - return result.to_media_player_item() + return result async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" From b07628ae577e53034dcee7093573fcf5337c8506 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Sep 2020 14:13:20 +0200 Subject: [PATCH 713/862] Rework Shelly sensors (#39747) * Rework Shelly sensors * Lint * Add shelly/entity to coveragerc --- .coveragerc | 1 + homeassistant/components/shelly/__init__.py | 59 +---- .../components/shelly/binary_sensor.py | 105 +++------ homeassistant/components/shelly/entity.py | 204 ++++++++++++++++++ homeassistant/components/shelly/light.py | 3 +- homeassistant/components/shelly/sensor.py | 180 ++++++---------- homeassistant/components/shelly/switch.py | 6 +- 7 files changed, 309 insertions(+), 249 deletions(-) create mode 100644 homeassistant/components/shelly/entity.py diff --git a/.coveragerc b/.coveragerc index 271e1f0c7ec..8fdbc410af2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -759,6 +759,7 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py homeassistant/components/shelly/binary_sensor.py + homeassistant/components/shelly/entity.py homeassistant/components/shelly/light.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/switch.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 85c8879756f..cfc566fdb3e 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -14,14 +14,9 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import ( - aiohttp_client, - device_registry, - entity, - update_coordinator, -) +from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator from .const import DOMAIN @@ -135,56 +130,6 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): await self.shutdown() -class ShellyBlockEntity(entity.Entity): - """Helper class to represent a block.""" - - def __init__(self, wrapper: ShellyDeviceWrapper, block): - """Initialize Shelly entity.""" - self.wrapper = wrapper - self.block = block - self._name = f"{self.wrapper.name} - {self.block.description.replace('_', ' ')}" - - @property - def name(self): - """Name of entity.""" - return self._name - - @property - def should_poll(self): - """If device should be polled.""" - return False - - @property - def device_info(self): - """Device info.""" - return { - "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} - } - - @property - def available(self): - """Available.""" - return self.wrapper.last_update_success - - @property - def unique_id(self): - """Return unique ID of entity.""" - return f"{self.wrapper.mac}-{self.block.description}" - - async def async_added_to_hass(self): - """When entity is added to HASS.""" - self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) - - async def async_update(self): - """Update entity with latest info.""" - await self.wrapper.async_request_refresh() - - @callback - def _update_callback(self): - """Handle device update.""" - self.async_write_ha_state() - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 80e935168f0..c1c241a32ef 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -1,6 +1,4 @@ """Binary sensor for Shelly.""" -import aioshelly - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, @@ -10,88 +8,47 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) -from . import ShellyBlockEntity, ShellyDeviceWrapper -from .const import DOMAIN +from .entity import ( + BlockAttributeDescription, + ShellyBlockAttributeEntity, + async_setup_entry_attribute_entities, +) SENSORS = { - "dwIsOpened": DEVICE_CLASS_OPENING, - "flood": DEVICE_CLASS_MOISTURE, - "gas": DEVICE_CLASS_GAS, - "overpower": None, - "overtemp": None, - "smoke": DEVICE_CLASS_SMOKE, - "vibration": DEVICE_CLASS_VIBRATION, + ("device", "overtemp"): BlockAttributeDescription(name="overtemp"), + ("relay", "overpower"): BlockAttributeDescription(name="overpower"), + ("sensor", "dwIsOpened"): BlockAttributeDescription( + name="Door", device_class=DEVICE_CLASS_OPENING + ), + ("sensor", "flood"): BlockAttributeDescription( + name="flood", device_class=DEVICE_CLASS_MOISTURE + ), + ("sensor", "gas"): BlockAttributeDescription( + name="gas", + device_class=DEVICE_CLASS_GAS, + value=lambda value: value in ["mild", "heavy"], + device_state_attributes=lambda block: {"detected": block.gas}, + ), + ("sensor", "smoke"): BlockAttributeDescription( + name="smoke", device_class=DEVICE_CLASS_SMOKE + ), + ("sensor", "vibration"): BlockAttributeDescription( + name="vibration", device_class=DEVICE_CLASS_VIBRATION + ), } async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for device.""" - wrapper = hass.data[DOMAIN][config_entry.entry_id] - sensors = [] - - for block in wrapper.device.blocks: - for attr in SENSORS: - if not hasattr(block, attr): - continue - - sensors.append(ShellySensor(wrapper, block, attr)) - - if sensors: - async_add_entities(sensors) + await async_setup_entry_attribute_entities( + hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor + ) -class ShellySensor(ShellyBlockEntity, BinarySensorEntity): - """Switch that controls a relay block on Shelly devices.""" - - def __init__( - self, - wrapper: ShellyDeviceWrapper, - block: aioshelly.Block, - attribute: str, - ) -> None: - """Initialize sensor.""" - super().__init__(wrapper, block) - self.attribute = attribute - device_class = SENSORS[attribute] - - self._device_class = device_class - - @property - def unique_id(self): - """Return unique ID of entity.""" - return f"{super().unique_id}-{self.attribute}" - - @property - def name(self): - """Name of sensor.""" - return f"{self.wrapper.name} - {self.attribute}" +class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity): + """Shelly binary sensor entity.""" @property def is_on(self): """Return true if sensor state is on.""" - if self.attribute == "gas": - # Gas sensor value of Shelly Gas can be none/mild/heavy/test. We return True - # when the value is mild or heavy. - return getattr(self.block, self.attribute) in ["mild", "heavy"] - return bool(getattr(self.block, self.attribute)) - - @property - def device_class(self): - """Device class of sensor.""" - return self._device_class - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self.attribute == "gas": - # We return raw value of the gas sensor as an attribute. - return {"detected": getattr(self.block, self.attribute)} - - @property - def available(self): - """Available.""" - if self.attribute == "gas": - # "sensorOp" is "normal" when Shelly Gas is working properly and taking - # measurements. - return super().available and self.block.sensorOp == "normal" - return super().available + return bool(self.attribute_value) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py new file mode 100644 index 00000000000..c7619ab7383 --- /dev/null +++ b/homeassistant/components/shelly/entity.py @@ -0,0 +1,204 @@ +"""Shelly entity helper.""" +from collections import Counter +from dataclasses import dataclass +from typing import Any, Callable, Optional, Union + +import aioshelly + +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback +from homeassistant.helpers import device_registry, entity + +from . import ShellyDeviceWrapper +from .const import DOMAIN + + +def temperature_unit(block_info: dict) -> str: + """Detect temperature unit.""" + if block_info[aioshelly.BLOCK_VALUE_UNIT] == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + +async def async_setup_entry_attribute_entities( + hass, config_entry, async_add_entities, sensors, sensor_class +): + """Set up entities for block attributes.""" + wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][config_entry.entry_id] + blocks = [] + + for block in wrapper.device.blocks: + for sensor_id in block.sensor_ids: + description = sensors.get((block.type, sensor_id)) + if description is None: + continue + + # Filter out non-existing sensors and sensors without a value + if getattr(block, sensor_id, None) in (-1, None): + continue + + blocks.append((block, sensor_id, description)) + + if not blocks: + return + + counts = Counter([item[0].type for item in blocks]) + + async_add_entities( + [ + sensor_class(wrapper, block, sensor_id, description, counts[block.type]) + for block, sensor_id, description in blocks + ] + ) + + +@dataclass +class BlockAttributeDescription: + """Class to describe a sensor.""" + + name: str + # Callable = lambda attr_info: unit + unit: Union[None, str, Callable[[dict], str]] = None + value: Callable[[Any], Any] = lambda val: val + device_class: Optional[str] = None + default_enabled: bool = True + available: Optional[Callable[[aioshelly.Block], bool]] = None + device_state_attributes: Optional[ + Callable[[aioshelly.Block], Optional[dict]] + ] = None + + +class ShellyBlockEntity(entity.Entity): + """Helper class to represent a block.""" + + def __init__(self, wrapper: ShellyDeviceWrapper, block): + """Initialize Shelly entity.""" + self.wrapper = wrapper + self.block = block + self._name = f"{self.wrapper.name} {self.block.description.replace('_', ' ')}" + + @property + def name(self): + """Name of entity.""" + return self._name + + @property + def should_poll(self): + """If device should be polled.""" + return False + + @property + def device_info(self): + """Device info.""" + return { + "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} + } + + @property + def available(self): + """Available.""" + return self.wrapper.last_update_success + + @property + def unique_id(self): + """Return unique ID of entity.""" + return f"{self.wrapper.mac}-{self.block.description}" + + async def async_added_to_hass(self): + """When entity is added to HASS.""" + self.async_on_remove(self.wrapper.async_add_listener(self._update_callback)) + + async def async_update(self): + """Update entity with latest info.""" + await self.wrapper.async_request_refresh() + + @callback + def _update_callback(self): + """Handle device update.""" + self.async_write_ha_state() + + +class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): + """Switch that controls a relay block on Shelly devices.""" + + def __init__( + self, + wrapper: ShellyDeviceWrapper, + block: aioshelly.Block, + attribute: str, + description: BlockAttributeDescription, + same_type_count: int, + ) -> None: + """Initialize sensor.""" + super().__init__(wrapper, block) + self.attribute = attribute + self.description = description + self.info = block.info(attribute) + + unit = self.description.unit + + if callable(unit): + unit = unit(self.info) + + self._unit = unit + self._unique_id = f"{super().unique_id}-{self.attribute}" + + name_parts = [self.wrapper.name] + if same_type_count > 1: + name_parts.append(str(block.index)) + name_parts.append(self.description.name) + + self._name = " ".join(name_parts) + + @property + def unique_id(self): + """Return unique ID of entity.""" + return self._unique_id + + @property + def name(self): + """Name of sensor.""" + return self._name + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if it should be enabled by default.""" + return self.description.default_enabled + + @property + def attribute_value(self): + """Value of sensor.""" + value = getattr(self.block, self.attribute) + + if value is None: + return None + + return self.description.value(value) + + @property + def unit_of_measurement(self): + """Return unit of sensor.""" + return self._unit + + @property + def device_class(self): + """Device class of sensor.""" + return self.description.device_class + + @property + def available(self): + """Available.""" + available = super().available + + if not available or not self.description.available: + return available + + return self.description.available(self.block) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.description.device_state_attributes is None: + return None + + return self.description.device_state_attributes(self.block) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 9e9b9e350a0..06004b40198 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -4,8 +4,9 @@ from aioshelly import Block from homeassistant.components.light import SUPPORT_BRIGHTNESS, LightEntity from homeassistant.core import callback -from . import ShellyBlockEntity, ShellyDeviceWrapper +from . import ShellyDeviceWrapper from .const import DOMAIN +from .entity import ShellyBlockEntity async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 618abc994b1..8e648a1a269 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -1,6 +1,4 @@ """Sensor for Shelly.""" -import aioshelly - from homeassistant.components import sensor from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, @@ -9,128 +7,84 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - VOLT, ) -from homeassistant.helpers.entity import Entity -from . import ShellyBlockEntity, ShellyDeviceWrapper -from .const import DOMAIN +from .entity import ( + BlockAttributeDescription, + ShellyBlockAttributeEntity, + async_setup_entry_attribute_entities, + temperature_unit, +) SENSORS = { - "battery": [PERCENTAGE, sensor.DEVICE_CLASS_BATTERY], - "concentration": [CONCENTRATION_PARTS_PER_MILLION, None], - "current": [ELECTRICAL_CURRENT_AMPERE, sensor.DEVICE_CLASS_CURRENT], - "deviceTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], - "energy": [ENERGY_KILO_WATT_HOUR, sensor.DEVICE_CLASS_ENERGY], - "energyReturned": [ENERGY_KILO_WATT_HOUR, sensor.DEVICE_CLASS_ENERGY], - "extTemp": [None, sensor.DEVICE_CLASS_TEMPERATURE], - "humidity": [PERCENTAGE, sensor.DEVICE_CLASS_HUMIDITY], - "luminosity": ["lx", sensor.DEVICE_CLASS_ILLUMINANCE], - "overpowerValue": [POWER_WATT, sensor.DEVICE_CLASS_POWER], - "power": [POWER_WATT, sensor.DEVICE_CLASS_POWER], - "powerFactor": [PERCENTAGE, sensor.DEVICE_CLASS_POWER_FACTOR], - "tilt": [DEGREE, None], - "voltage": [VOLT, sensor.DEVICE_CLASS_VOLTAGE], + ("device", "battery"): BlockAttributeDescription( + name="Battery", unit=PERCENTAGE, device_class=sensor.DEVICE_CLASS_BATTERY + ), + ("device", "deviceTemp"): BlockAttributeDescription( + name="Device Temperature", + unit=temperature_unit, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_TEMPERATURE, + default_enabled=False, + ), + ("emeter", "current"): BlockAttributeDescription( + name="Current", + unit=ELECTRICAL_CURRENT_AMPERE, + value=lambda value: value, + device_class=sensor.DEVICE_CLASS_CURRENT, + ), + ("light", "power"): BlockAttributeDescription( + name="Power", + unit=POWER_WATT, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_POWER, + default_enabled=False, + ), + ("relay", "energy"): BlockAttributeDescription( + name="Energy", + unit=ENERGY_KILO_WATT_HOUR, + value=lambda value: round(value / 60 / 1000, 2), + device_class=sensor.DEVICE_CLASS_ENERGY, + ), + ("sensor", "concentration"): BlockAttributeDescription( + name="Gas Concentration", + unit=CONCENTRATION_PARTS_PER_MILLION, + value=lambda value: value, + # "sensorOp" is "normal" when the Shelly Gas is working properly and taking measurements. + available=lambda block: block.sensorOp == "normal", + ), + ("sensor", "extTemp"): BlockAttributeDescription( + name="Temperature", + unit=temperature_unit, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_TEMPERATURE, + ), + ("sensor", "humidity"): BlockAttributeDescription( + name="Humidity", + unit=PERCENTAGE, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_HUMIDITY, + ), + ("sensor", "luminosity"): BlockAttributeDescription( + name="Luminosity", + unit="lx", + device_class=sensor.DEVICE_CLASS_ILLUMINANCE, + ), + ("sensor", "tilt"): BlockAttributeDescription(name="tilt", unit=DEGREE), } async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for device.""" - wrapper = hass.data[DOMAIN][config_entry.entry_id] - sensors = [] - - for block in wrapper.device.blocks: - for attr in SENSORS: - # Filter out non-existing sensors and sensors without a value - if getattr(block, attr, None) is None: - continue - - sensors.append(ShellySensor(wrapper, block, attr)) - - if sensors: - async_add_entities(sensors) + await async_setup_entry_attribute_entities( + hass, config_entry, async_add_entities, SENSORS, ShellySensor + ) -class ShellySensor(ShellyBlockEntity, Entity): - """Switch that controls a relay block on Shelly devices.""" - - def __init__( - self, - wrapper: ShellyDeviceWrapper, - block: aioshelly.Block, - attribute: str, - ) -> None: - """Initialize sensor.""" - super().__init__(wrapper, block) - self.attribute = attribute - unit, device_class = SENSORS[attribute] - self.info = block.info(attribute) - - if ( - self.info[aioshelly.BLOCK_VALUE_TYPE] - == aioshelly.BLOCK_VALUE_TYPE_TEMPERATURE - ): - if self.info[aioshelly.BLOCK_VALUE_UNIT] == "C": - unit = TEMP_CELSIUS - else: - unit = TEMP_FAHRENHEIT - - self._unit = unit - self._device_class = device_class - - @property - def unique_id(self): - """Return unique ID of entity.""" - return f"{super().unique_id}-{self.attribute}" - - @property - def name(self): - """Name of sensor.""" - return f"{self.wrapper.name} - {self.attribute}" +class ShellySensor(ShellyBlockAttributeEntity): + """Represent a shelly sensor.""" @property def state(self): - """Value of sensor.""" - value = getattr(self.block, self.attribute) - if value is None: - return None - - if self.attribute in ["luminosity", "tilt"]: - return round(value) - if self.attribute in [ - "deviceTemp", - "extTemp", - "humidity", - "overpowerValue", - "power", - ]: - return round(value, 1) - if self.attribute == "powerFactor": - return round(value * 100, 1) - # Energy unit change from Wmin or Wh to kWh - if self.info.get(aioshelly.BLOCK_VALUE_UNIT) == "Wmin": - return round(value / 60 / 1000, 2) - if self.info.get(aioshelly.BLOCK_VALUE_UNIT) == "Wh": - return round(value / 1000, 2) - return value - - @property - def unit_of_measurement(self): - """Return unit of sensor.""" - return self._unit - - @property - def device_class(self): - """Device class of sensor.""" - return self._device_class - - @property - def available(self): - """Available.""" - if self.attribute == "concentration": - # "sensorOp" is "normal" when the Shelly Gas is working properly and taking - # measurements. - return super().available and self.block.sensorOp == "normal" - return super().available + """Return value of sensor.""" + return self.attribute_value diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 7dd57467045..1c3c48637e9 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -4,17 +4,15 @@ from aioshelly import RelayBlock from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback -from . import ShellyBlockEntity, ShellyDeviceWrapper +from . import ShellyDeviceWrapper from .const import DOMAIN +from .entity import ShellyBlockEntity async def async_setup_entry(hass, config_entry, async_add_entities): """Set up switches for device.""" wrapper = hass.data[DOMAIN][config_entry.entry_id] - if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": - return - relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"] if not relay_blocks: From f6aa3e0cdc538ed550adbeb230326df120a5cafb Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Sep 2020 14:13:47 +0200 Subject: [PATCH 714/862] Improve Plugwise config_options testing (#39736) --- tests/components/plugwise/test_config_flow.py | 69 +++++++++---------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 6044381ac51..11c0000977a 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -3,7 +3,6 @@ from Plugwise_Smile.Smile import Smile import pytest from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.plugwise import config_flow from homeassistant.components.plugwise.const import DEFAULT_SCAN_INTERVAL, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL @@ -168,21 +167,6 @@ async def test_form_other_problem(hass, mock_smile): assert result2["errors"] == {"base": "unknown"} -async def test_show_zeroconf_form(hass, mock_smile) -> None: - """Test that the zeroconf confirmation form is served.""" - flow = config_flow.PlugwiseConfigFlow() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - result = await flow.async_step_zeroconf(TEST_DISCOVERY) - - await hass.async_block_till_done() - assert flow.context["title_placeholders"][CONF_HOST] == TEST_HOST - assert flow.context["title_placeholders"]["name"] == "P1 DSMR v1.2.3" - - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - async def test_options_flow_power(hass, mock_smile) -> None: """Test config flow options DSMR environments.""" entry = MockConfigEntry( @@ -195,18 +179,24 @@ async def test_options_flow_power(hass, mock_smile) -> None: hass.data[DOMAIN] = {entry.entry_id: {"api": MagicMock(smile_type="power")}} entry.add_to_hass(hass) - result = await hass.config_entries.options.async_init(entry.entry_id) + with patch( + "homeassistant.components.plugwise.async_setup_entry", return_value=True + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SCAN_INTERVAL: 10} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == { - CONF_SCAN_INTERVAL: 10, - } + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 10} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_SCAN_INTERVAL: 10, + } async def test_options_flow_thermo(hass, mock_smile) -> None: @@ -221,15 +211,22 @@ async def test_options_flow_thermo(hass, mock_smile) -> None: hass.data[DOMAIN] = {entry.entry_id: {"api": MagicMock(smile_type="thermostat")}} entry.add_to_hass(hass) - result = await hass.config_entries.options.async_init(entry.entry_id) + with patch( + "homeassistant.components.plugwise.async_setup_entry", return_value=True + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SCAN_INTERVAL: 60} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == { - CONF_SCAN_INTERVAL: 60, - } + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 60} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_SCAN_INTERVAL: 60, + } From 007873153e46ef4c0110842d32cf77cd9b6d8ad8 Mon Sep 17 00:00:00 2001 From: Markus Bong <2Fake1987@gmail.com> Date: Mon, 7 Sep 2020 14:24:05 +0200 Subject: [PATCH 715/862] Add devolo current and total consumption to sensors (#38386) --- .../components/devolo_home_control/sensor.py | 80 +++++++++++++++++-- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/devolo_home_control/sensor.py b/homeassistant/components/devolo_home_control/sensor.py index bdf0f05528d..4bb2536dcc2 100644 --- a/homeassistant/components/devolo_home_control/sensor.py +++ b/homeassistant/components/devolo_home_control/sensor.py @@ -4,6 +4,7 @@ import logging from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ) from homeassistant.config_entries import ConfigEntry @@ -18,6 +19,8 @@ DEVICE_CLASS_MAPPING = { "temperature": DEVICE_CLASS_TEMPERATURE, "light": DEVICE_CLASS_ILLUMINANCE, "humidity": DEVICE_CLASS_HUMIDITY, + "current": DEVICE_CLASS_POWER, + "total": DEVICE_CLASS_POWER, } @@ -36,17 +39,39 @@ async def async_setup_entry( element_uid=multi_level_sensor, ) ) + for device in hass.data[DOMAIN]["homecontrol"].devices.values(): + if hasattr(device, "consumption_property"): + for consumption in device.consumption_property: + for consumption_type in ["current", "total"]: + entities.append( + DevoloConsumptionEntity( + homecontrol=hass.data[DOMAIN]["homecontrol"], + device_instance=device, + element_uid=consumption, + consumption=consumption_type, + ) + ) async_add_entities(entities, False) class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity): - """Representation o a multi level sensor within devolo Home Control.""" + """Representation of a multi level sensor within devolo Home Control.""" - def __init__(self, homecontrol, device_instance, element_uid): + def __init__( + self, + homecontrol, + device_instance, + element_uid, + multi_level_sensor_property=None, + sync=None, + ): """Initialize a devolo multi level sensor.""" - self._multi_level_sensor_property = device_instance.multi_level_sensor_property[ - element_uid - ] + if multi_level_sensor_property is None: + self._multi_level_sensor_property = ( + device_instance.multi_level_sensor_property[element_uid] + ) + else: + self._multi_level_sensor_property = multi_level_sensor_property self._state = self._multi_level_sensor_property.value @@ -66,7 +91,7 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity): device_instance=device_instance, element_uid=element_uid, name=name, - sync=self._sync, + sync=self._sync if sync is None else sync, ) @property @@ -95,3 +120,46 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity): else: _LOGGER.debug("No valid message received: %s", message) self.schedule_update_ha_state() + + +class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity): + """Representation of a consumption entity within devolo Home Control.""" + + def __init__(self, homecontrol, device_instance, element_uid, consumption): + """Initialize a devolo consumption sensor.""" + self._device_instance = device_instance + + self.value = getattr( + device_instance.consumption_property[element_uid], consumption + ) + self.sensor_type = consumption + self.unit = getattr( + device_instance.consumption_property[element_uid], f"{consumption}_unit" + ) + self.element_uid = element_uid + + super().__init__( + homecontrol, + device_instance, + element_uid, + multi_level_sensor_property=self, + sync=self._sync, + ) + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"{self._unique_id}_{self.sensor_type}" + + def _sync(self, message=None): + """Update the consumption sensor state.""" + if message[0] == self.element_uid: + self._state = getattr( + self._device_instance.consumption_property[self.element_uid], + self.sensor_type, + ) + elif message[0].startswith("hdm"): + self._available = self._device_instance.is_online() + else: + _LOGGER.debug("No valid message received: %s", message) + self.schedule_update_ha_state() From 7c56ee8e0ccee2014018ee482ea337ed92313a91 Mon Sep 17 00:00:00 2001 From: Markus Bong <2Fake1987@gmail.com> Date: Mon, 7 Sep 2020 15:48:14 +0200 Subject: [PATCH 716/862] Add devolo remote devices as binary sensors (#39105) --- .../devolo_home_control/binary_sensor.py | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index d3da333b407..e05350dbbac 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -41,7 +41,20 @@ async def async_setup_entry( element_uid=binary_sensor, ) ) - + for device in hass.data[DOMAIN]["homecontrol"].devices.values(): + if hasattr(device, "remote_control_property"): + for remote in device.remote_control_property: + for index in range( + 1, device.remote_control_property[remote].key_count + 1 + ): + entities.append( + DevoloRemoteControl( + homecontrol=hass.data[DOMAIN]["homecontrol"], + device_instance=device, + element_uid=remote, + key=index, + ) + ) async_add_entities(entities, False) @@ -97,3 +110,48 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): else: _LOGGER.debug("No valid message received: %s", message) self.schedule_update_ha_state() + + +class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity): + """Representation of a remote control within devolo Home Control.""" + + def __init__(self, homecontrol, device_instance, element_uid, key): + """Initialize a devolo remote control.""" + self._remote_control_property = device_instance.remote_control_property.get( + element_uid + ) + super().__init__( + homecontrol=homecontrol, + device_instance=device_instance, + element_uid=f"{element_uid}_{key}", + name=device_instance.item_name, + sync=self._sync, + ) + + self._key = key + + self._state = False + + self._subscriber = None + + @property + def is_on(self): + """Return the state.""" + return self._state + + def _sync(self, message=None): + """Update the binary sensor state.""" + if ( + message[0] == self._remote_control_property.element_uid + and message[1] == self._key + ): + self._state = True + elif ( + message[0] == self._remote_control_property.element_uid and message[1] == 0 + ): + self._state = False + elif message[0].startswith("hdm"): + self._available = self._device_instance.is_online() + else: + _LOGGER.debug("No valid message received: %s", message) + self.schedule_update_ha_state() From 8a3279a5c9ac28a43da2832c8af52da13523c54f Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 7 Sep 2020 08:54:18 -0500 Subject: [PATCH 717/862] Add device action support for remotes (#39400) --- .../components/remote/device_action.py | 30 +++ .../components/remote/device_condition.py | 38 +++ .../components/remote/device_trigger.py | 38 +++ homeassistant/components/remote/strings.json | 15 ++ tests/components/remote/test_device_action.py | 143 +++++++++++ .../remote/test_device_condition.py | 235 ++++++++++++++++++ .../components/remote/test_device_trigger.py | 234 +++++++++++++++++ .../custom_components/test/remote.py | 39 +++ 8 files changed, 772 insertions(+) create mode 100644 homeassistant/components/remote/device_action.py create mode 100644 homeassistant/components/remote/device_condition.py create mode 100644 homeassistant/components/remote/device_trigger.py create mode 100644 tests/components/remote/test_device_action.py create mode 100644 tests/components/remote/test_device_condition.py create mode 100644 tests/components/remote/test_device_trigger.py create mode 100644 tests/testing_config/custom_components/test/remote.py diff --git a/homeassistant/components/remote/device_action.py b/homeassistant/components/remote/device_action.py new file mode 100644 index 00000000000..aa819f3eb46 --- /dev/null +++ b/homeassistant/components/remote/device_action.py @@ -0,0 +1,30 @@ +"""Provides device actions for remotes.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Change state based on configuration.""" + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/remote/device_condition.py b/homeassistant/components/remote/device_condition.py new file mode 100644 index 00000000000..06c7bec89d4 --- /dev/null +++ b/homeassistant/components/remote/device_condition.py @@ -0,0 +1,38 @@ +"""Provides device conditions for remotes.""" +from typing import Dict, List + +import voluptuous as vol + +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.condition import ConditionCheckerType +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +@callback +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> ConditionCheckerType: + """Evaluate state based on configuration.""" + if config_validation: + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return await toggle_entity.async_get_condition_capabilities(hass, config) diff --git a/homeassistant/components/remote/device_trigger.py b/homeassistant/components/remote/device_trigger.py new file mode 100644 index 00000000000..5919e8c61ba --- /dev/null +++ b/homeassistant/components/remote/device_trigger.py @@ -0,0 +1,38 @@ +"""Provides device triggers for remotes.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, config) diff --git a/homeassistant/components/remote/strings.json b/homeassistant/components/remote/strings.json index eb9edcd450f..bb7df2e89ef 100644 --- a/homeassistant/components/remote/strings.json +++ b/homeassistant/components/remote/strings.json @@ -1,5 +1,20 @@ { "title": "Remote", + "device_automation": { + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}" + }, + "condition_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" + } + }, "state": { "_": { "off": "[%key:common::state::off%]", diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py new file mode 100644 index 00000000000..89d0f0de6bf --- /dev/null +++ b/tests/components/remote/test_device_action.py @@ -0,0 +1,143 @@ +"""The test for remote device automation.""" +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.components.remote import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a remote.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "toggle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass, calls): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_off", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_on", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggle", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py new file mode 100644 index 00000000000..2f01b4ab55f --- /dev/null +++ b/tests/components/remote/test_device_condition.py @@ -0,0 +1,235 @@ +"""The test for remote device automation.""" +from datetime import timedelta + +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.remote import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.async_mock import patch +from tests.common import ( + MockConfigEntry, + async_get_device_automation_capabilities, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a remote.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a remote condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + ent1, ent2, ent3 = platform.ENTITIES + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py new file mode 100644 index 00000000000..73feb3ea08f --- /dev/null +++ b/tests/components/remote/test_device_trigger.py @@ -0,0 +1,234 @@ +"""The test for remote device automation.""" +from datetime import timedelta + +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.remote import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + async_get_device_automation_capabilities, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a remote.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a remote trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( + ent1.entity_id + ) + + hass.states.async_set(ent1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( + ent1.entity_id + ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format( + ent1.entity_id + ) diff --git a/tests/testing_config/custom_components/test/remote.py b/tests/testing_config/custom_components/test/remote.py new file mode 100644 index 00000000000..c6f05b156f3 --- /dev/null +++ b/tests/testing_config/custom_components/test/remote.py @@ -0,0 +1,39 @@ +""" +Provide a mock remote platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.remote import RemoteEntity +from homeassistant.const import STATE_OFF, STATE_ON + +from tests.common import MockToggleEntity + +ENTITIES = [] + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockRemote("TV", STATE_ON), + MockRemote("DVD", STATE_OFF), + MockRemote(None, STATE_OFF), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES) + + +class MockRemote(MockToggleEntity, RemoteEntity): + """Mock remote class.""" + + supported_features = 0 From 4b84b74b8946e2a54f1b6714625f7ef623bc16a0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 7 Sep 2020 16:08:24 +0200 Subject: [PATCH 718/862] Support state trigger with from/for but no to (#39480) --- .../homeassistant/triggers/state.py | 8 ++- .../homeassistant/triggers/test_state.py | 58 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index c580b8daf49..dad7314f4fa 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -37,7 +37,10 @@ TRIGGER_SCHEMA = vol.All( vol.Optional(CONF_ATTRIBUTE): cv.match_all, } ), - cv.key_dependency(CONF_FOR, CONF_TO), + vol.Any( + cv.key_dependency(CONF_FOR, CONF_TO), + cv.key_dependency(CONF_FOR, CONF_FROM), + ), ) @@ -141,6 +144,9 @@ async def async_attach_trigger( else: cur_value = new_st.attributes.get(attribute) + if CONF_TO not in config: + return cur_value != old_value + return cur_value == new_value unsub_track_same[entity] = async_track_same_state( diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 9cce567ca68..0ae7c340ea8 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -925,6 +925,64 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_change_from_with_for(hass, calls): + """Test for firing on change with from/for.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "media_player.foo", + "from": "playing", + "for": "00:00:30", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.states.async_set("media_player.foo", "playing") + await hass.async_block_till_done() + hass.states.async_set("media_player.foo", "paused") + await hass.async_block_till_done() + hass.states.async_set("media_player.foo", "stopped") + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_if_not_fires_on_change_from_with_for(hass, calls): + """Test for firing on change with from/for.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "media_player.foo", + "from": "playing", + "for": "00:00:30", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.states.async_set("media_player.foo", "playing") + await hass.async_block_till_done() + hass.states.async_set("media_player.foo", "paused") + await hass.async_block_till_done() + hass.states.async_set("media_player.foo", "playing") + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) + await hass.async_block_till_done() + assert len(calls) == 0 + + async def test_invalid_for_template_1(hass, calls): """Test for invalid for template.""" assert await async_setup_component( From 8c96eb7c5602b5e0adf7688b8ac6ba127bc663d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2020 09:16:29 -0500 Subject: [PATCH 719/862] Retry tado setup later when cloud service is unavailable (#39748) --- homeassistant/components/tado/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 9927db80a65..44a0f551ae0 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -91,6 +91,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except RuntimeError as exc: _LOGGER.error("Failed to setup tado: %s", exc) return ConfigEntryNotReady + except requests.exceptions.Timeout as ex: + raise ConfigEntryNotReady from ex except requests.exceptions.HTTPError as ex: if ex.response.status_code > 400 and ex.response.status_code < 500: _LOGGER.error("Failed to login to tado: %s", ex) From 7c86fa0203c94d48a4b1623828989bf7bf1de4b5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Sep 2020 16:30:02 +0200 Subject: [PATCH 720/862] Support 'for' without setting the 'to'-state in automation state triggers (#39730) --- .../homeassistant/triggers/state.py | 26 ++++----- .../homeassistant/triggers/test_state.py | 56 ++++++++++++------- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index dad7314f4fa..0fa7a98b562 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -25,22 +25,16 @@ CONF_ENTITY_ID = "entity_id" CONF_FROM = "from" CONF_TO = "to" -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_PLATFORM): "state", - vol.Required(CONF_ENTITY_ID): cv.entity_ids, - # These are str on purpose. Want to catch YAML conversions - vol.Optional(CONF_FROM): vol.Any(str, [str]), - vol.Optional(CONF_TO): vol.Any(str, [str]), - vol.Optional(CONF_FOR): cv.positive_time_period_template, - vol.Optional(CONF_ATTRIBUTE): cv.match_all, - } - ), - vol.Any( - cv.key_dependency(CONF_FOR, CONF_TO), - cv.key_dependency(CONF_FOR, CONF_FROM), - ), +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "state", + vol.Required(CONF_ENTITY_ID): cv.entity_ids, + # These are str on purpose. Want to catch YAML conversions + vol.Optional(CONF_FROM): vol.Any(str, [str]), + vol.Optional(CONF_TO): vol.Any(str, [str]), + vol.Optional(CONF_FOR): cv.positive_time_period_template, + vol.Optional(CONF_ATTRIBUTE): cv.match_all, + } ) diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 0ae7c340ea8..68ce907bdae 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -334,25 +334,6 @@ async def test_if_fails_setup_bad_for(hass, calls): assert mock_logger.error.called -async def test_if_fails_setup_for_without_to(hass, calls): - """Test for setup failures for missing to.""" - with assert_setup_component(0, automation.DOMAIN): - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: { - "trigger": { - "platform": "state", - "entity_id": "test.entity", - "for": {"seconds": 5}, - }, - "action": {"service": "homeassistant.turn_on"}, - } - }, - ) - - async def test_if_not_fires_on_entity_change_with_for(hass, calls): """Test for not firing on entity change with for.""" assert await async_setup_component( @@ -520,6 +501,43 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): assert 1 == len(calls) +async def test_if_fires_on_entity_change_with_for_without_to(hass, calls): + """Test for firing on entity change with for.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "for": {"seconds": 5}, + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + hass.states.async_set("test.entity", "hello") + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set("test.entity", "world") + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=4)) + await hass.async_block_till_done() + assert len(calls) == 0 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_if_fires_on_entity_creation_and_removal(hass, calls): """Test for firing on entity creation and removal, with to/from constraints.""" # set automations for multiple combinations to/from From fc2cddc4524ead5ec9b3abb14bedc5b26c78c0f3 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 7 Sep 2020 16:57:22 +0200 Subject: [PATCH 721/862] Fix netatmo browse_media return type (#39751) --- .../components/netatmo/media_source.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index d5dcb2e6059..09c34ec41af 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -1,12 +1,13 @@ """Netatmo Media Source Implementation.""" import datetime as dt +import logging import re from typing import Optional, Tuple from homeassistant.components.media_player.const import MEDIA_TYPE_VIDEO from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.const import MEDIA_MIME_TYPES -from homeassistant.components.media_source.error import Unresolvable +from homeassistant.components.media_source.error import MediaSourceError, Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, MediaSource, @@ -17,9 +18,14 @@ from homeassistant.core import HomeAssistant, callback from .const import DATA_CAMERAS, DATA_EVENTS, DOMAIN, MANUFACTURER +_LOGGER = logging.getLogger(__name__) MIME_TYPE = "application/x-mpegURL" +class IncompatibleMediaSource(MediaSourceError): + """Incompatible media source attributes.""" + + async def async_get_media_source(hass: HomeAssistant): """Set up Netatmo media source.""" return NetatmoSource(hass) @@ -44,7 +50,7 @@ class NetatmoSource(MediaSource): async def async_browse_media( self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES - ) -> Optional[BrowseMediaSource]: + ) -> BrowseMediaSource: """Return media.""" try: source, camera_id, event_id = async_parse_identifier(item) @@ -55,7 +61,7 @@ class NetatmoSource(MediaSource): def _browse_media( self, source: str, camera_id: str, event_id: int - ) -> Optional[BrowseMediaSource]: + ) -> BrowseMediaSource: """Browse media.""" if camera_id and camera_id not in self.events: raise BrowseError("Camera does not exist.") @@ -67,7 +73,7 @@ class NetatmoSource(MediaSource): def _build_item_response( self, source: str, camera_id: str, event_id: int = None - ) -> Optional[BrowseMediaSource]: + ) -> BrowseMediaSource: if event_id and event_id in self.events[camera_id]: created = dt.datetime.fromtimestamp(event_id) thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url") @@ -95,7 +101,10 @@ class NetatmoSource(MediaSource): ) if not media.can_play and not media.can_expand: - return None + _LOGGER.debug( + "Camera %s with event %s without media url found", camera_id, event_id + ) + raise IncompatibleMediaSource if not media.can_expand: return media @@ -109,7 +118,10 @@ class NetatmoSource(MediaSource): media.children.append(child) else: for eid in self.events[camera_id]: - child = self._build_item_response(source, camera_id, eid) + try: + child = self._build_item_response(source, camera_id, eid) + except IncompatibleMediaSource: + continue if child: media.children.append(child) From 90d574e521f7278fe3a91ca2f293669613e7d21d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2020 10:19:39 -0500 Subject: [PATCH 722/862] Ensure static templates are still called back on first refresh (#39753) --- homeassistant/helpers/template.py | 5 ++-- tests/helpers/test_event.py | 40 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ac326711581..c771992caa4 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -245,7 +245,7 @@ class Template: def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str: """Render given template.""" if self.is_static: - return self.template + return self.template.strip() if variables is not None: kwargs.update(variables) @@ -261,7 +261,7 @@ class Template: This method must be run in the event loop. """ if self.is_static: - return self.template + return self.template.strip() compiled = self._compiled or self._ensure_compiled() @@ -284,6 +284,7 @@ class Template: # pylint: disable=protected-access if self.is_static: + render_info._result = self.template.strip() render_info._freeze_static() return render_info diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 41d252177a4..cc06c0fd19c 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1035,6 +1035,46 @@ async def test_track_template_result_errors(hass, caplog): assert isinstance(not_exist_runs[2][3], TemplateError) +async def test_static_string(hass): + """Test a static string.""" + template_refresh = Template("{{ 'static' }}", hass) + + refresh_runs = [] + + @ha.callback + def refresh_listener(event, updates): + refresh_runs.append(updates.pop().result) + + info = async_track_template_result( + hass, [TrackTemplate(template_refresh, None)], refresh_listener + ) + await hass.async_block_till_done() + info.async_refresh() + await hass.async_block_till_done() + + assert refresh_runs == ["static"] + + +async def test_string(hass): + """Test a string.""" + template_refresh = Template("no_template", hass) + + refresh_runs = [] + + @ha.callback + def refresh_listener(event, updates): + refresh_runs.append(updates.pop().result) + + info = async_track_template_result( + hass, [TrackTemplate(template_refresh, None)], refresh_listener + ) + await hass.async_block_till_done() + info.async_refresh() + await hass.async_block_till_done() + + assert refresh_runs == ["no_template"] + + async def test_track_template_result_refresh_cancel(hass): """Test cancelling and refreshing result.""" template_refresh = Template("{{states.switch.test.state == 'on' and now() }}", hass) From ae5d8f4d64870b522e47319b30b48cc32b2a0ab4 Mon Sep 17 00:00:00 2001 From: Tomasz Date: Mon, 7 Sep 2020 17:29:51 +0200 Subject: [PATCH 723/862] Support color temperature (#39743) Co-authored-by: Paulus Schoutsen --- homeassistant/components/shelly/entity.py | 2 +- homeassistant/components/shelly/light.py | 67 +++++++++++++++++++---- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index c7619ab7383..b1a61bbbf59 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -145,7 +145,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): name_parts = [self.wrapper.name] if same_type_count > 1: - name_parts.append(str(block.index)) + name_parts.append(str(block.channel)) name_parts.append(self.description.name) self._name = " ".join(name_parts) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 06004b40198..8025fd11205 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -1,8 +1,20 @@ """Light for Shelly.""" +from typing import Optional + from aioshelly import Block -from homeassistant.components.light import SUPPORT_BRIGHTNESS, LightEntity +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, + LightEntity, +) from homeassistant.core import callback +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) from . import ShellyDeviceWrapper from .const import DOMAIN @@ -30,6 +42,13 @@ class ShellyLight(ShellyBlockEntity, LightEntity): self._supported_features = 0 if hasattr(block, "brightness"): self._supported_features |= SUPPORT_BRIGHTNESS + if hasattr(block, "colorTemp"): + self._supported_features |= SUPPORT_COLOR_TEMP + + @property + def supported_features(self) -> int: + """Supported features.""" + return self._supported_features @property def is_on(self) -> bool: @@ -40,7 +59,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return self.block.output @property - def brightness(self): + def brightness(self) -> Optional[int]: """Brightness of light.""" if self.control_result: brightness = self.control_result["brightness"] @@ -49,21 +68,47 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return int(brightness / 100 * 255) @property - def supported_features(self): - """Supported features.""" - return self._supported_features + def color_temp(self) -> Optional[float]: + """Return the CT color value in mireds.""" + if self.control_result: + color_temp = self.control_result["temp"] + else: + color_temp = self.block.colorTemp - async def async_turn_on( - self, brightness=None, **kwargs - ): # pylint: disable=arguments-differ + # If you set DUO to max mireds in Shelly app, 2700K, + # It reports 0 temp + if color_temp == 0: + return self.max_mireds + + return int(color_temperature_kelvin_to_mired(color_temp)) + + @property + def min_mireds(self) -> float: + """Return the coldest color_temp that this light supports.""" + return color_temperature_kelvin_to_mired(6500) + + @property + def max_mireds(self) -> float: + """Return the warmest color_temp that this light supports.""" + return color_temperature_kelvin_to_mired(2700) + + async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" params = {"turn": "on"} - if brightness is not None: - params["brightness"] = int(brightness / 255 * 100) + if ATTR_BRIGHTNESS in kwargs: + tmp_brightness = kwargs[ATTR_BRIGHTNESS] + params["brightness"] = int(tmp_brightness / 255 * 100) + if ATTR_COLOR_TEMP in kwargs: + color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + if color_temp > 6500: + color_temp = 6500 + elif color_temp < 2700: + color_temp = 2700 + params["temp"] = int(color_temp) self.control_result = await self.block.set_state(**params) self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn off light.""" self.control_result = await self.block.set_state(turn="off") self.async_write_ha_state() From 5e83feeabf93915bc6397d18596ae26a98f0c734 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2020 10:33:22 -0500 Subject: [PATCH 724/862] Increase test coverage for template sandbox (#39750) --- tests/helpers/test_template.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 6a1a4e6f58c..c1f018b47d6 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2306,3 +2306,18 @@ def test_is_template_string(): assert template.is_template_string("{# a comment #} Hey") is True assert template.is_template_string("1") is False assert template.is_template_string("Some Text") is False + + +async def test_protected_blocked(hass): + """Test accessing __getattr__ produces a template error.""" + tmp = template.Template('{{ states.__getattr__("any") }}', hass) + with pytest.raises(TemplateError): + tmp.async_render() + + tmp = template.Template('{{ states.sensor.__getattr__("any") }}', hass) + with pytest.raises(TemplateError): + tmp.async_render() + + tmp = template.Template('{{ states.sensor.any.__getattr__("any") }}', hass) + with pytest.raises(TemplateError): + tmp.async_render() From 84992da24a50d9b7724acdf647b4547be14242f1 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 7 Sep 2020 17:52:35 +0200 Subject: [PATCH 725/862] Make Spotify library payload use "BrowseMedia" (#39744) --- homeassistant/components/spotify/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index ccc51ad41a6..e7b3d4a4552 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -598,7 +598,7 @@ def library_payload(): {"name": item["name"], "type": item["type"], "uri": item["type"]} ) ) - return library_info + return BrowseMedia(**library_info) def fetch_image_url(item, key="images"): From c11b88b4c2c1f8b3a40474ff764ae0a44dc8d536 Mon Sep 17 00:00:00 2001 From: Marijn Pool Date: Mon, 7 Sep 2020 18:08:46 +0200 Subject: [PATCH 726/862] Remove remaining and add finishes_at attributes from timer (#37519) --- homeassistant/components/timer/__init__.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index e47ac69be5b..1cc5bb3265d 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -1,5 +1,4 @@ """Support for Timers.""" -from datetime import timedelta import logging import typing @@ -31,6 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" DEFAULT_DURATION = 0 ATTR_DURATION = "duration" ATTR_REMAINING = "remaining" +ATTR_FINISHES_AT = "finishes_at" CONF_DURATION = "duration" STATUS_IDLE = "idle" @@ -184,7 +184,7 @@ class Timer(RestoreEntity): self._config = config self.editable = True self._state = STATUS_IDLE - self._remaining = config[CONF_DURATION] + self._remaining = None self._end = None self._listener = None @@ -224,11 +224,16 @@ class Timer(RestoreEntity): @property def state_attributes(self): """Return the state attributes.""" - return { + attrs = { ATTR_DURATION: str(self._config[CONF_DURATION]), ATTR_EDITABLE: self.editable, - ATTR_REMAINING: str(self._remaining), } + if self._end is not None: + attrs[ATTR_FINISHES_AT] = str(self._end) + if self._remaining is not None: + attrs[ATTR_REMAINING] = str(self._remaining) + + return attrs @property def unique_id(self) -> typing.Optional[str]: @@ -299,7 +304,7 @@ class Timer(RestoreEntity): self._listener = None self._state = STATUS_IDLE self._end = None - self._remaining = timedelta() + self._remaining = None self.hass.bus.async_fire(EVENT_TIMER_CANCELLED, {"entity_id": self.entity_id}) self.async_write_ha_state() @@ -311,7 +316,8 @@ class Timer(RestoreEntity): self._listener = None self._state = STATUS_IDLE - self._remaining = timedelta() + self._end = None + self._remaining = None self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) self.async_write_ha_state() @@ -323,7 +329,8 @@ class Timer(RestoreEntity): self._listener = None self._state = STATUS_IDLE - self._remaining = timedelta() + self._end = None + self._remaining = None self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) self.async_write_ha_state() From ef8cdf04059869c7463b23a52afc4f029945ac9b Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 7 Sep 2020 18:22:20 +0200 Subject: [PATCH 727/862] Add Kodi media browser support (#39729) Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + homeassistant/components/kodi/browse_media.py | 216 ++++++++++++++++++ homeassistant/components/kodi/media_player.py | 75 +++++- 3 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/kodi/browse_media.py diff --git a/.coveragerc b/.coveragerc index 8fdbc410af2..56a52b4f4e5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -446,6 +446,7 @@ omit = homeassistant/components/knx/climate.py homeassistant/components/knx/cover.py homeassistant/components/kodi/__init__.py + homeassistant/components/kodi/browse_media.py homeassistant/components/kodi/const.py homeassistant/components/kodi/media_player.py homeassistant/components/kodi/notify.py diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py new file mode 100644 index 00000000000..d308b48101b --- /dev/null +++ b/homeassistant/components/kodi/browse_media.py @@ -0,0 +1,216 @@ +"""Support for media browsing.""" + +from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_SEASON, + MEDIA_TYPE_TRACK, + MEDIA_TYPE_TVSHOW, +) + +PLAYABLE_MEDIA_TYPES = [ + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_TRACK, +] + +EXPANDABLE_MEDIA_TYPES = [ + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_SEASON, +] + + +async def build_item_response(media_library, payload): + """Create response payload for the provided media query.""" + search_id = payload["search_id"] + search_type = payload["search_type"] + + thumbnail = None + title = None + media = None + + query = {"properties": ["thumbnail"]} + # pylint: disable=protected-access + if search_type == MEDIA_TYPE_ALBUM: + if search_id: + query.update({"filter": {"albumid": int(search_id)}}) + query["properties"].extend( + ["albumid", "artist", "duration", "album", "track"] + ) + album = await media_library._server.AudioLibrary.GetAlbumDetails( + {"albumid": int(search_id), "properties": ["thumbnail"]} + ) + thumbnail = media_library.thumbnail_url( + album["albumdetails"].get("thumbnail") + ) + title = album["albumdetails"]["label"] + media = await media_library._server.AudioLibrary.GetSongs(query) + media = media.get("songs") + else: + media = await media_library._server.AudioLibrary.GetAlbums(query) + media = media.get("albums") + title = "Albums" + elif search_type == MEDIA_TYPE_ARTIST: + if search_id: + query.update({"filter": {"artistid": int(search_id)}}) + media = await media_library._server.AudioLibrary.GetAlbums(query) + media = media.get("albums") + artist = await media_library._server.AudioLibrary.GetArtistDetails( + {"artistid": int(search_id), "properties": ["thumbnail"]} + ) + thumbnail = media_library.thumbnail_url( + artist["artistdetails"].get("thumbnail") + ) + title = artist["artistdetails"]["label"] + else: + media = await media_library._server.AudioLibrary.GetArtists(query) + media = media.get("artists") + title = "Artists" + elif search_type == "library_music": + library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"} + media = [{"label": name, "type": type_} for type_, name in library.items()] + title = "Music Library" + elif search_type == MEDIA_TYPE_MOVIE: + media = await media_library._server.VideoLibrary.GetMovies(query) + media = media.get("movies") + title = "Movies" + elif search_type == MEDIA_TYPE_TVSHOW: + if search_id: + media = await media_library._server.VideoLibrary.GetSeasons( + { + "tvshowid": int(search_id), + "properties": ["thumbnail", "season", "tvshowid"], + } + ) + media = media.get("seasons") + tvshow = await media_library._server.VideoLibrary.GetTVShowDetails( + {"tvshowid": int(search_id), "properties": ["thumbnail"]} + ) + thumbnail = media_library.thumbnail_url( + tvshow["tvshowdetails"].get("thumbnail") + ) + title = tvshow["tvshowdetails"]["label"] + else: + media = await media_library._server.VideoLibrary.GetTVShows(query) + media = media.get("tvshows") + title = "TV Shows" + elif search_type == MEDIA_TYPE_SEASON: + tv_show_id, season_id = search_id.split("/", 1) + media = await media_library._server.VideoLibrary.GetEpisodes( + { + "tvshowid": int(tv_show_id), + "season": int(season_id), + "properties": ["thumbnail", "tvshowid", "seasonid"], + } + ) + media = media.get("episodes") + if media: + season = await media_library._server.VideoLibrary.GetSeasonDetails( + {"seasonid": int(media[0]["seasonid"]), "properties": ["thumbnail"]} + ) + thumbnail = media_library.thumbnail_url( + season["seasondetails"].get("thumbnail") + ) + title = season["seasondetails"]["label"] + + if media is None: + return + + return BrowseMedia( + media_content_id=payload["search_id"], + media_content_type=search_type, + title=title, + can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id, + can_expand=True, + children=[item_payload(item, media_library) for item in media], + thumbnail=thumbnail, + ) + + +def item_payload(item, media_library): + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + if "songid" in item: + media_content_type = MEDIA_TYPE_TRACK + media_content_id = f"{item['songid']}" + elif "albumid" in item: + media_content_type = MEDIA_TYPE_ALBUM + media_content_id = f"{item['albumid']}" + elif "artistid" in item: + media_content_type = MEDIA_TYPE_ARTIST + media_content_id = f"{item['artistid']}" + elif "movieid" in item: + media_content_type = MEDIA_TYPE_MOVIE + media_content_id = f"{item['movieid']}" + elif "episodeid" in item: + media_content_type = MEDIA_TYPE_EPISODE + media_content_id = f"{item['episodeid']}" + elif "seasonid" in item: + media_content_type = MEDIA_TYPE_SEASON + media_content_id = f"{item['tvshowid']}/{item['season']}" + elif "tvshowid" in item: + media_content_type = MEDIA_TYPE_TVSHOW + media_content_id = f"{item['tvshowid']}" + else: + # this case is for the top folder of each type + # possible content types: album, artist, movie, library_music, tvshow + media_content_type = item.get("type") + media_content_id = "" + + title = item["label"] + can_play = media_content_type in PLAYABLE_MEDIA_TYPES and bool(media_content_id) + can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES + + thumbnail = item.get("thumbnail") + if thumbnail: + thumbnail = media_library.thumbnail_url(thumbnail) + + return BrowseMedia( + title=title, + media_content_type=media_content_type, + media_content_id=media_content_id, + can_play=can_play, + can_expand=can_expand, + thumbnail=thumbnail, + ) + + +def library_payload(media_library): + """ + Create response payload to describe contents of a specific library. + + Used by async_browse_media. + """ + library_info = BrowseMedia( + media_content_id="library", + media_content_type="library", + title="Media Library", + can_play=False, + can_expand=True, + children=[], + ) + + library = { + "library_music": "Music", + MEDIA_TYPE_MOVIE: "Movies", + MEDIA_TYPE_TVSHOW: "TV shows", + } + for item in [{"label": name, "type": type_} for type_, name in library.items()]: + library_info.children.append( + item_payload( + {"label": item["label"], "type": item["type"], "uri": item["type"]}, + media_library, + ) + ) + + return library_info diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 96095767a01..ce05f3fc732 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -9,12 +9,18 @@ import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_TYPE_ALBUM, + MEDIA_TYPE_ARTIST, MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_SEASON, + MEDIA_TYPE_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, + SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -29,6 +35,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) +from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, @@ -50,6 +57,7 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.dt as dt_util +from .browse_media import build_item_response, library_payload from .const import ( CONF_WS_PORT, DATA_CONNECTION, @@ -103,20 +111,28 @@ MEDIA_TYPES = { "audio": MEDIA_TYPE_MUSIC, } +MAP_KODI_MEDIA_TYPES = { + MEDIA_TYPE_MOVIE: "movieid", + MEDIA_TYPE_EPISODE: "episodeid", + MEDIA_TYPE_SEASON: "seasonid", + MEDIA_TYPE_TVSHOW: "tvshowid", +} + SUPPORT_KODI = ( - SUPPORT_PAUSE - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_MUTE - | SUPPORT_PREVIOUS_TRACK + SUPPORT_BROWSE_MEDIA | SUPPORT_NEXT_TRACK - | SUPPORT_SEEK - | SUPPORT_PLAY_MEDIA - | SUPPORT_STOP - | SUPPORT_SHUFFLE_SET + | SUPPORT_PAUSE | SUPPORT_PLAY - | SUPPORT_VOLUME_STEP + | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SEEK + | SUPPORT_SHUFFLE_SET + | SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP ) @@ -644,6 +660,31 @@ class KodiEntity(MediaPlayerEntity): await self._kodi.play_playlist(int(media_id)) elif media_type_lower == "directory": await self._kodi.play_directory(str(media_id)) + elif media_type_lower in [ + MEDIA_TYPE_ARTIST, + MEDIA_TYPE_ALBUM, + ]: + await self.async_clear_playlist() + params = {"playlistid": 0, "item": {f"{media_type}id": int(media_id)}} + # pylint: disable=protected-access + await self._kodi._server.Playlist.Add(params) + await self._kodi.play_playlist(0) + elif media_type_lower == MEDIA_TYPE_TRACK: + await self._kodi.clear_playlist() + params = {"playlistid": 0, "item": {"songid": int(media_id)}} + # pylint: disable=protected-access + await self._kodi._server.Playlist.Add(params) + await self._kodi.play_playlist(0) + elif media_type_lower in [ + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_SEASON, + MEDIA_TYPE_TVSHOW, + ]: + # pylint: disable=protected-access + await self._kodi._play_item( + {MAP_KODI_MEDIA_TYPES[media_type_lower]: int(media_id)} + ) else: await self._kodi.play_file(str(media_id)) @@ -794,3 +835,19 @@ class KodiEntity(MediaPlayerEntity): out[i][1] = rate return sorted(out, key=lambda out: out[1], reverse=True) + + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + if media_content_type in [None, "library"]: + return await self.hass.async_add_executor_job(library_payload, self._kodi) + + payload = { + "search_type": media_content_type, + "search_id": media_content_id, + } + response = await build_item_response(self._kodi, payload) + if response is None: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) + return response From 4b01ab616ad7b9e467e34859a933cd684d1bef07 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Sep 2020 19:12:52 +0200 Subject: [PATCH 728/862] Timer make attribute format always h:mm:ss (#38292) Co-authored-by: Paulus Schoutsen --- homeassistant/components/timer/__init__.py | 70 +++++++++++++--------- tests/components/timer/test_init.py | 7 ++- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 1cc5bb3265d..64d651b4cd8 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -1,6 +1,7 @@ """Support for Timers.""" +from datetime import datetime, timedelta import logging -import typing +from typing import Dict, Optional import voluptuous as vol @@ -64,6 +65,13 @@ UPDATE_FIELDS = { } +def _format_timedelta(delta: timedelta): + total_seconds = delta.total_seconds() + hours, remainder = divmod(total_seconds, 3600) + minutes, seconds = divmod(remainder, 60) + return f"{int(hours)}:{int(minutes):02}:{int(seconds):02}" + + def _none_to_empty_dict(value): if value is None: return {} @@ -78,9 +86,9 @@ CONFIG_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, - vol.Optional( - CONF_DURATION, default=DEFAULT_DURATION - ): cv.time_period, + vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.All( + cv.time_period, _format_timedelta + ), }, ) ) @@ -156,40 +164,42 @@ class TimerStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + async def _process_create_data(self, data: Dict) -> Dict: """Validate the config is valid.""" data = self.CREATE_SCHEMA(data) # make duration JSON serializeable - data[CONF_DURATION] = str(data[CONF_DURATION]) + data[CONF_DURATION] = _format_timedelta(data[CONF_DURATION]) return data @callback - def _get_suggested_id(self, info: typing.Dict) -> str: + def _get_suggested_id(self, info: Dict) -> str: """Suggest an ID based on the config.""" return info[CONF_NAME] - async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + async def _update_data(self, data: dict, update_data: Dict) -> Dict: """Return a new updated data object.""" data = {**data, **self.UPDATE_SCHEMA(update_data)} # make duration JSON serializeable - data[CONF_DURATION] = str(data[CONF_DURATION]) + if CONF_DURATION in update_data: + data[CONF_DURATION] = _format_timedelta(data[CONF_DURATION]) return data class Timer(RestoreEntity): """Representation of a timer.""" - def __init__(self, config: typing.Dict): + def __init__(self, config: Dict): """Initialize a timer.""" - self._config = config - self.editable = True - self._state = STATUS_IDLE - self._remaining = None - self._end = None + self._config: dict = config + self.editable: bool = True + self._state: str = STATUS_IDLE + self._duration = cv.time_period_str(config[CONF_DURATION]) + self._remaining: Optional[timedelta] = None + self._end: Optional[datetime] = None self._listener = None @classmethod - def from_yaml(cls, config: typing.Dict) -> "Timer": + def from_yaml(cls, config: Dict) -> "Timer": """Return entity instance initialized from yaml storage.""" timer = cls(config) timer.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) @@ -225,18 +235,18 @@ class Timer(RestoreEntity): def state_attributes(self): """Return the state attributes.""" attrs = { - ATTR_DURATION: str(self._config[CONF_DURATION]), + ATTR_DURATION: _format_timedelta(self._duration), ATTR_EDITABLE: self.editable, } if self._end is not None: - attrs[ATTR_FINISHES_AT] = str(self._end) + attrs[ATTR_FINISHES_AT] = self._end.isoformat() if self._remaining is not None: - attrs[ATTR_REMAINING] = str(self._remaining) + attrs[ATTR_REMAINING] = _format_timedelta(self._remaining) return attrs @property - def unique_id(self) -> typing.Optional[str]: + def unique_id(self) -> Optional[str]: """Return unique id for the entity.""" return self._config[CONF_ID] @@ -250,7 +260,7 @@ class Timer(RestoreEntity): self._state = state and state.state == state @callback - def async_start(self, duration): + def async_start(self, duration: timedelta): """Start a timer.""" if self._listener: self._listener() @@ -265,15 +275,18 @@ class Timer(RestoreEntity): self._state = STATUS_ACTIVE start = dt_util.utcnow().replace(microsecond=0) + if self._remaining and newduration is None: self._end = start + self._remaining + + elif newduration: + self._duration = newduration + self._remaining = newduration + self._end = start + self._duration + else: - if newduration: - self._config[CONF_DURATION] = newduration - self._remaining = newduration - else: - self._remaining = self._config[CONF_DURATION] - self._end = start + self._config[CONF_DURATION] + self._remaining = self._duration + self._end = start + self._duration self.hass.bus.async_fire(event, {"entity_id": self.entity_id}) @@ -334,7 +347,8 @@ class Timer(RestoreEntity): self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) self.async_write_ha_state() - async def async_update_config(self, config: typing.Dict) -> None: + async def async_update_config(self, config: Dict) -> None: """Handle when the config is updated.""" self._config = config + self._duration = cv.time_period_str(config[CONF_DURATION]) self.async_write_ha_state() diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 75bafba634f..6b5a2596b71 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -24,6 +24,7 @@ from homeassistant.components.timer import ( STATUS_ACTIVE, STATUS_IDLE, STATUS_PAUSED, + _format_timedelta, ) from homeassistant.const import ( ATTR_EDITABLE, @@ -61,7 +62,7 @@ def storage_setup(hass, hass_storage): { ATTR_ID: "from_storage", ATTR_NAME: "timer from storage", - ATTR_DURATION: 0, + ATTR_DURATION: "0:00:00", } ] }, @@ -544,7 +545,7 @@ async def test_update(hass, hass_ws_client, storage_setup): assert resp["success"] state = hass.states.get(timer_entity_id) - assert state.attributes[ATTR_DURATION] == str(cv.time_period(33)) + assert state.attributes[ATTR_DURATION] == _format_timedelta(cv.time_period(33)) async def test_ws_create(hass, hass_ws_client, storage_setup): @@ -574,7 +575,7 @@ async def test_ws_create(hass, hass_ws_client, storage_setup): state = hass.states.get(timer_entity_id) assert state.state == STATUS_IDLE - assert state.attributes[ATTR_DURATION] == str(cv.time_period(42)) + assert state.attributes[ATTR_DURATION] == _format_timedelta(cv.time_period(42)) assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, timer_id) == timer_entity_id From 78dbd090b5b7e766c862eb989887ad968c0ec236 Mon Sep 17 00:00:00 2001 From: On Freund Date: Mon, 7 Sep 2020 20:15:00 +0300 Subject: [PATCH 729/862] Update pykodi to 0.1.2 (#39758) --- homeassistant/components/kodi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 7141ca43346..b3794d5dfa2 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,7 +2,7 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", - "requirements": ["pykodi==0.1.1"], + "requirements": ["pykodi==0.1.2"], "codeowners": [ "@OnFreund" ], diff --git a/requirements_all.txt b/requirements_all.txt index b74db09ad41..16c124cc4a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1427,7 +1427,7 @@ pyitachip2ir==0.0.7 pykira==0.1.1 # homeassistant.components.kodi -pykodi==0.1.1 +pykodi==0.1.2 # homeassistant.components.kwb pykwb==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d09fe2d07ac..9f1a62faa2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -692,7 +692,7 @@ pyisy==2.0.2 pykira==0.1.1 # homeassistant.components.kodi -pykodi==0.1.1 +pykodi==0.1.2 # homeassistant.components.lastfm pylast==3.3.0 From 36a1877cbd4af8d6282110a227072de02e698a9a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 7 Sep 2020 19:24:06 +0200 Subject: [PATCH 730/862] Add missing MENU option for webos.button service (#39746) See all button options at https://github.com/bendavid/aiopylgtv/blob/master/aiopylgtv/buttons.py. --- homeassistant/components/webostv/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index b88b3d839d7..5719e339793 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -9,7 +9,7 @@ button: button: description: >- Name of the button to press. Known possible values are - LEFT, RIGHT, DOWN, UP, HOME, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, + LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 example: "LEFT" From be8aa161708bbda46c89be9aa71262d8df4ddef4 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 7 Sep 2020 13:52:00 -0400 Subject: [PATCH 731/862] Don't poll entities for unavailable ZHA devices (#39756) * Don't poll entities for unavailable ZHA devices * Update homeassistant/components/zha/entity.py Co-authored-by: Bas Nijholt * cleanup after accepting suggestion Co-authored-by: Bas Nijholt --- homeassistant/components/zha/entity.py | 10 +++++++--- homeassistant/components/zha/light.py | 7 ++----- homeassistant/components/zha/sensor.py | 6 ++++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 309691dd3df..96f005ba288 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -202,9 +202,13 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): async def async_update(self) -> None: """Retrieve latest state.""" - for channel in self.cluster_channels.values(): - if hasattr(channel, "async_update"): - await channel.async_update() + tasks = [ + channel.async_update() + for channel in self.cluster_channels.values() + if hasattr(channel, "async_update") + ] + if tasks: + await asyncio.gather(*tasks) class ZhaGroupEntity(BaseZhaEntity): diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index ff080562190..ba05d63df12 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -410,13 +410,10 @@ class Light(BaseLight, ZhaEntity): if "effect" in last_state.attributes: self._effect = last_state.attributes["effect"] - async def async_update(self): - """Attempt to retrieve on off state from the light.""" - await super().async_update() - await self.async_get_state() - async def async_get_state(self, from_cache=True): """Attempt to retrieve on off state from the light.""" + if not from_cache and not self.available: + return self.debug("polling current state - from cache: %s", from_cache) if self._on_off_channel: state = await self._on_off_channel.get_attribute_value( diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 9f507275836..6e2878f371b 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -210,6 +210,12 @@ class ElectricalMeasurement(Sensor): return round(value, self._decimals) return round(value) + async def async_update(self) -> None: + """Retrieve latest state.""" + if not self.available: + return + await super().async_update() + @STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER) @STRICT_MATCH(channel_names=CHANNEL_HUMIDITY) From d0e44893f524078ddede151ea589831728a6954e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Sep 2020 20:38:06 +0200 Subject: [PATCH 732/862] Local media source: return different error if media folder doesnt exist (#39759) --- homeassistant/components/media_source/local_source.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 34b526171ea..d670cee1676 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -83,6 +83,8 @@ class LocalSource(MediaSource): full_path = Path(self.hass.config.path("media", location)) if not full_path.exists(): + if location == "": + raise BrowseError("Media directory does not exist.") raise BrowseError("Path does not exist.") if not full_path.is_dir(): From 6f43285a28157653495dc5e2e159da1c47f80114 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 7 Sep 2020 20:26:58 +0100 Subject: [PATCH 733/862] Force token expires_in to float (#39489) --- .../helpers/config_entry_oauth2_flow.py | 10 ++++ .../helpers/test_config_entry_oauth2_flow.py | 56 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index b845db966bb..da86c222c13 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -25,6 +25,8 @@ from homeassistant.helpers.network import get_url from .aiohttp_client import async_get_clientsession +_LOGGER = logging.getLogger(__name__) + DATA_JWT_SECRET = "oauth2_jwt_secret" DATA_VIEW_REGISTERED = "oauth2_view_reg" DATA_IMPLEMENTATIONS = "oauth2_impl" @@ -77,6 +79,8 @@ class AbstractOAuth2Implementation(ABC): async def async_refresh_token(self, token: dict) -> dict: """Refresh a token and update expires info.""" new_token = await self._async_refresh_token(token) + # Force int for non-compliant oauth2 providers + new_token["expires_in"] = int(new_token["expires_in"]) new_token["expires_at"] = time.time() + new_token["expires_in"] return new_token @@ -257,6 +261,12 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): ) -> Dict[str, Any]: """Create config entry from external data.""" token = await self.flow_impl.async_resolve_external_data(self.external_data) + # Force int for non-compliant oauth2 providers + try: + token["expires_in"] = int(token["expires_in"]) + except ValueError as err: + _LOGGER.warning("Error converting expires_in to int: %s", err) + return self.async_abort(reason="oauth_error") token["expires_at"] = time.time() + token["expires_in"] self.logger.info("Successfully authenticated") diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 227f3e366f3..dc34b0f7876 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -128,6 +128,62 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): assert result["reason"] == "authorize_url_timeout" +async def test_abort_if_oauth_error( + hass, flow_handler, local_impl, aiohttp_client, aioclient_mock, current_request +): + """Check bad oauth token.""" + await async_process_ha_core_config( + hass, + {"external_url": "https://example.com"}, + ) + + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + TOKEN_URL, + json={ + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": "badnumber", + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "oauth_error" + + async def test_step_discovery(hass, flow_handler, local_impl): """Check flow triggers from discovery.""" await async_process_ha_core_config( From 72b392e8535dab8dcdb3a8082d79e2cb06035cd0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 7 Sep 2020 21:32:13 +0200 Subject: [PATCH 734/862] Update frontend to 20200907.0 (#39761) --- 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 377381e03c7..65d0c6d3a21 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200904.0"], + "requirements": ["home-assistant-frontend==20200907.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 983535ad7ce..5382e95b5b8 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.36.1 -home-assistant-frontend==20200904.0 +home-assistant-frontend==20200907.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 16c124cc4a9..f0198702f88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -746,7 +746,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200904.0 +home-assistant-frontend==20200907.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f1a62faa2d..d9a40e0284c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200904.0 +home-assistant-frontend==20200907.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From c3ee79e4db13af3809d4749ef1e94f6748189d6c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 7 Sep 2020 22:17:10 +0200 Subject: [PATCH 735/862] Clean up spotify media browser dict access (#39764) --- .../components/spotify/media_player.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index e7b3d4a4552..6f9791f1409 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -425,8 +425,8 @@ class SpotifyMediaPlayer(MediaPlayerEntity): def build_item_response(spotify, user, payload): """Create response payload for the provided media query.""" - media_content_type = payload.get("media_content_type") - media_content_id = payload.get("media_content_id") + media_content_type = payload["media_content_type"] + media_content_id = payload["media_content_id"] title = None image = None if media_content_type == "current_user_playlists": @@ -507,7 +507,7 @@ def build_item_response(spotify, user, payload): children=[ BrowseMedia( title=item.get("name"), - media_content_id=item.get("id"), + media_content_id=item["id"], media_content_type="category_playlists", thumbnail=fetch_image_url(item, key="icons"), can_play=False, @@ -547,25 +547,24 @@ def item_payload(item): Used by async_browse_media. """ if MEDIA_TYPE_TRACK in item: - item = item.get(MEDIA_TYPE_TRACK) + item = item[MEDIA_TYPE_TRACK] elif MEDIA_TYPE_SHOW in item: - item = item.get(MEDIA_TYPE_SHOW) + item = item[MEDIA_TYPE_SHOW] elif MEDIA_TYPE_ARTIST in item: - item = item.get(MEDIA_TYPE_ARTIST) - elif MEDIA_TYPE_ALBUM in item and item.get("type") != MEDIA_TYPE_TRACK: - item = item.get(MEDIA_TYPE_ALBUM) + item = item[MEDIA_TYPE_ARTIST] + elif MEDIA_TYPE_ALBUM in item and item["type"] != MEDIA_TYPE_TRACK: + item = item[MEDIA_TYPE_ALBUM] - can_expand = item.get("type") not in [ - None, + can_expand = item["type"] not in [ MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, ] payload = { "title": item.get("name"), - "media_content_id": item.get("uri"), - "media_content_type": item.get("type"), - "can_play": item.get("type") in PLAYABLE_MEDIA_TYPES, + "media_content_id": item["uri"], + "media_content_type": item["type"], + "can_play": item["type"] in PLAYABLE_MEDIA_TYPES, "can_expand": can_expand, } From 38834d194551bad73c81ff6a325eccae54178123 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 7 Sep 2020 22:58:21 +0200 Subject: [PATCH 736/862] Fix plex browse media (#39766) --- .../components/plex/media_browser.py | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 39ad44f5ff1..1d3f3616450 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -76,7 +76,7 @@ def browse_media( items = getattr(library_or_section, method)() for item in items: payload["children"].append(item_payload(item)) - return payload + return BrowseMedia(**payload) if media_content_type in ["server", None]: return server_payload(plex_server) @@ -114,46 +114,44 @@ def item_payload(item): def library_section_payload(section): """Create response payload for a single library section.""" - return { - "title": section.title, - "media_content_id": section.key, - "media_content_type": "library", - "can_play": False, - "can_expand": True, - } + return BrowseMedia( + title=section.title, + media_content_id=section.key, + media_content_type="library", + can_play=False, + can_expand=True, + ) def special_library_payload(parent_payload, special_type): """Create response payload for special library folders.""" - title = f"{special_type} ({parent_payload['title']})" - return { - "title": title, - "media_content_id": f"{parent_payload['media_content_id']}:{special_type}", - "media_content_type": parent_payload["media_content_type"], - "can_play": False, - "can_expand": True, - } + title = f"{special_type} ({parent_payload.title})" + return BrowseMedia( + title=title, + media_content_id=f"{parent_payload.media_content_id}:{special_type}", + media_content_type=parent_payload.media_content_type, + can_play=False, + can_expand=True, + ) def server_payload(plex_server): """Create response payload to describe libraries of the Plex server.""" - server_info = { - "title": plex_server.friendly_name, - "media_content_id": plex_server.machine_identifier, - "media_content_type": "server", - "can_play": False, - "can_expand": True, - } - server_info["children"] = [] - server_info["children"].append(special_library_payload(server_info, "On Deck")) - server_info["children"].append( - special_library_payload(server_info, "Recently Added") + server_info = BrowseMedia( + title=plex_server.friendly_name, + media_content_id=plex_server.machine_identifier, + media_content_type="server", + can_play=False, + can_expand=True, ) + server_info.children = [] + server_info.children.append(special_library_payload(server_info, "On Deck")) + server_info.children.append(special_library_payload(server_info, "Recently Added")) for library in plex_server.library.sections(): if library.type == "photo": continue - server_info["children"].append(library_section_payload(library)) - server_info["children"].append(PLAYLISTS_BROWSE_PAYLOAD) + server_info.children.append(library_section_payload(library)) + server_info.children.append(BrowseMedia(**PLAYLISTS_BROWSE_PAYLOAD)) return server_info @@ -161,13 +159,13 @@ def library_payload(plex_server, library_id): """Create response payload to describe contents of a specific library.""" library = plex_server.library.sectionByID(library_id) library_info = library_section_payload(library) - library_info["children"] = [] - library_info["children"].append(special_library_payload(library_info, "On Deck")) - library_info["children"].append( + library_info.children = [] + library_info.children.append(special_library_payload(library_info, "On Deck")) + library_info.children.append( special_library_payload(library_info, "Recently Added") ) for item in library.all(): - library_info["children"].append(item_payload(item)) + library_info.children.append(item_payload(item)) return library_info @@ -176,4 +174,4 @@ def playlists_payload(plex_server): playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []} for playlist in plex_server.playlists(): playlists_info["children"].append(item_payload(playlist)) - return playlists_info + return BrowseMedia(**playlists_info) From 7756038bee15a6c447317445421bd2bf7d4348de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 7 Sep 2020 23:01:27 +0200 Subject: [PATCH 737/862] Bumped version to 0.115.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ea5c203ee02..cb926177ba5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 214fc044738961c9b42251d3c2aad23922897689 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 8 Sep 2020 10:35:01 +0200 Subject: [PATCH 738/862] Support shelly cover(roller) mode (#39711) Co-authored-by: Paulus Schoutsen --- .coveragerc | 2 + homeassistant/components/shelly/__init__.py | 2 +- homeassistant/components/shelly/cover.py | 104 ++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/shelly/cover.py diff --git a/.coveragerc b/.coveragerc index 56a52b4f4e5..fe7a8d74cbd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -760,7 +760,9 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py homeassistant/components/shelly/binary_sensor.py + homeassistant/components/shelly/cover.py homeassistant/components/shelly/entity.py + homeassistant/components/shelly/cover.py homeassistant/components/shelly/light.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/switch.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index cfc566fdb3e..39fc34d6d56 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers import aiohttp_client, device_registry, update_coordi from .const import DOMAIN -PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "light", "sensor", "switch", "cover"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py new file mode 100644 index 00000000000..ec1933ba420 --- /dev/null +++ b/homeassistant/components/shelly/cover.py @@ -0,0 +1,104 @@ +"""Cover for Shelly.""" +from aioshelly import Block + +from homeassistant.components.cover import ( + ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverEntity, +) +from homeassistant.core import callback + +from . import ShellyDeviceWrapper +from .const import DOMAIN +from .entity import ShellyBlockEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up cover for device.""" + wrapper = hass.data[DOMAIN][config_entry.entry_id] + blocks = [block for block in wrapper.device.blocks if block.type == "roller"] + + if not blocks: + return + + async_add_entities(ShellyCover(wrapper, block) for block in blocks) + + +class ShellyCover(ShellyBlockEntity, CoverEntity): + """Switch that controls a cover block on Shelly devices.""" + + def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: + """Initialize light.""" + super().__init__(wrapper, block) + self.control_result = None + self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + if self.wrapper.device.settings["rollers"][0]["positioning"]: + self._supported_features |= SUPPORT_SET_POSITION + + @property + def is_closed(self): + """If cover is closed.""" + if self.control_result: + return self.control_result["current_pos"] == 0 + + return self.block.rollerPos == 0 + + @property + def current_cover_position(self): + """Position of the cover.""" + if self.control_result: + return self.control_result["current_pos"] + + return self.block.rollerPos + + @property + def is_closing(self): + """Return if the cover is closing.""" + if self.control_result: + return self.control_result["state"] == "close" + + return self.block.roller == "close" + + @property + def is_opening(self): + """Return if the cover is opening.""" + if self.control_result: + return self.control_result["state"] == "open" + + return self.block.roller == "open" + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_features + + async def async_close_cover(self, **kwargs): + """Close cover.""" + self.control_result = await self.block.set_state(go="close") + self.async_write_ha_state() + + async def async_open_cover(self, **kwargs): + """Open cover.""" + self.control_result = await self.block.set_state(go="open") + self.async_write_ha_state() + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + self.control_result = await self.block.set_state( + go="to_pos", roller_pos=kwargs[ATTR_POSITION] + ) + self.async_write_ha_state() + + async def async_stop_cover(self, **_kwargs): + """Stop the cover.""" + self.control_result = await self.block.set_state(go="stop") + self.async_write_ha_state() + + @callback + def _update_callback(self): + """When device updates, clear control result that overrides state.""" + self.control_result = None + super()._update_callback() From 71c25574055ae8c85f0f771c22225a8695693dbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:23:29 +0200 Subject: [PATCH 739/862] Guard for spotify items without type (#39795) Co-authored-by: Bram Kragten --- .../components/spotify/media_player.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 6f9791f1409..cc62cb9e276 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -437,16 +437,16 @@ def build_item_response(spotify, user, payload): items = media.get("artists", {}).get("items", []) elif media_content_type == "current_user_saved_albums": media = spotify.current_user_saved_albums(limit=BROWSE_LIMIT) - items = media.get("items", []) + items = [item["album"] for item in media.get("items", [])] elif media_content_type == "current_user_saved_tracks": media = spotify.current_user_saved_tracks(limit=BROWSE_LIMIT) - items = media.get("items", []) + items = [item["track"] for item in media.get("items", [])] elif media_content_type == "current_user_saved_shows": media = spotify.current_user_saved_shows(limit=BROWSE_LIMIT) - items = media.get("items", []) + items = [item["show"] for item in media.get("items", [])] elif media_content_type == "current_user_recently_played": media = spotify.current_user_recently_played(limit=BROWSE_LIMIT) - items = media.get("items", []) + items = [item["track"] for item in media.get("items", [])] elif media_content_type == "current_user_top_artists": media = spotify.current_user_top_artists(limit=BROWSE_LIMIT) items = media.get("items", []) @@ -474,7 +474,7 @@ def build_item_response(spotify, user, payload): items = media.get("albums", {}).get("items", []) elif media_content_type == MEDIA_TYPE_PLAYLIST: media = spotify.playlist(media_content_id) - items = media.get("tracks", {}).get("items", []) + items = [item["track"] for item in media.get("tracks", {}).get("items", [])] elif media_content_type == MEDIA_TYPE_ALBUM: media = spotify.album(media_content_id) items = media.get("tracks", {}).get("items", []) @@ -546,14 +546,6 @@ def item_payload(item): Used by async_browse_media. """ - if MEDIA_TYPE_TRACK in item: - item = item[MEDIA_TYPE_TRACK] - elif MEDIA_TYPE_SHOW in item: - item = item[MEDIA_TYPE_SHOW] - elif MEDIA_TYPE_ARTIST in item: - item = item[MEDIA_TYPE_ARTIST] - elif MEDIA_TYPE_ALBUM in item and item["type"] != MEDIA_TYPE_TRACK: - item = item[MEDIA_TYPE_ALBUM] can_expand = item["type"] not in [ MEDIA_TYPE_TRACK, From 2a879afc7ab70a91259ed3eb078884e48e240fa7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 8 Sep 2020 16:42:01 +0200 Subject: [PATCH 740/862] Add media class browse media attribute (#39770) --- .../components/arcam_fmj/media_player.py | 4 + homeassistant/components/kodi/browse_media.py | 21 +++++ .../components/media_player/__init__.py | 4 + .../components/media_player/const.py | 23 ++++++ .../components/media_source/local_source.py | 2 + .../components/media_source/models.py | 4 + .../components/netatmo/media_source.py | 6 +- .../components/philips_js/media_player.py | 4 + .../components/plex/media_browser.py | 65 ++++++++++++++-- homeassistant/components/roku/media_player.py | 12 +++ .../components/sonos/media_player.py | 30 ++++++++ .../components/spotify/media_player.py | 77 ++++++++++++++++--- tests/components/media_source/test_init.py | 2 + tests/components/media_source/test_models.py | 12 ++- 14 files changed, 248 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 7e6c34a8324..1f0f564c59b 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -7,6 +7,8 @@ from arcam.fmj.state import State from homeassistant import config_entries from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MUSIC, MEDIA_TYPE_MUSIC, SUPPORT_BROWSE_MEDIA, SUPPORT_PLAY_MEDIA, @@ -255,6 +257,7 @@ class ArcamFmj(MediaPlayerEntity): radio = [ BrowseMedia( title=preset.name, + media_class=MEDIA_CLASS_MUSIC, media_content_id=f"preset:{preset.index}", media_content_type=MEDIA_TYPE_MUSIC, can_play=True, @@ -265,6 +268,7 @@ class ArcamFmj(MediaPlayerEntity): root = BrowseMedia( title="Root", + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="root", media_content_type="library", can_play=False, diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index d308b48101b..1b1576e82da 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -2,6 +2,14 @@ from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MOVIE, + MEDIA_CLASS_MUSIC, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_SEASON, + MEDIA_CLASS_TV_SHOW, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_EPISODE, @@ -26,6 +34,16 @@ EXPANDABLE_MEDIA_TYPES = [ MEDIA_TYPE_SEASON, ] +CONTENT_TYPE_MEDIA_CLASS = { + "library_music": MEDIA_CLASS_MUSIC, + MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, + MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, + MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE, + MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, +} + async def build_item_response(media_library, payload): """Create response payload for the provided media query.""" @@ -124,6 +142,7 @@ async def build_item_response(media_library, payload): return return BrowseMedia( + media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], media_content_id=payload["search_id"], media_content_type=search_type, title=title, @@ -177,6 +196,7 @@ def item_payload(item, media_library): return BrowseMedia( title=title, + media_class=CONTENT_TYPE_MEDIA_CLASS[item["type"]], media_content_type=media_content_type, media_content_id=media_content_id, can_play=can_play, @@ -192,6 +212,7 @@ def library_payload(media_library): Used by async_browse_media. """ library_info = BrowseMedia( + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", media_content_type="library", title="Media Library", diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 16cabe5edb9..718011b4a76 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -822,6 +822,7 @@ class MediaPlayerEntity(Entity): Payload should follow this format: { "title": str - Title of the item + "media_class": str - Media class "media_content_type": str - see below "media_content_id": str - see below - Can be passed back in to browse further @@ -1046,6 +1047,7 @@ class BrowseMedia: def __init__( self, *, + media_class: str, media_content_id: str, media_content_type: str, title: str, @@ -1055,6 +1057,7 @@ class BrowseMedia: thumbnail: Optional[str] = None, ): """Initialize browse media item.""" + self.media_class = media_class self.media_content_id = media_content_id self.media_content_type = media_content_type self.title = title @@ -1067,6 +1070,7 @@ class BrowseMedia: """Convert Media class to browse media dictionary.""" response = { "title": self.title, + "media_class": self.media_class, "media_content_type": self.media_content_type, "media_content_id": self.media_content_id, "can_play": self.can_play, diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 74c65fcd780..6714d03c19e 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -29,6 +29,29 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list" DOMAIN = "media_player" +MEDIA_CLASS_ALBUM = "album" +MEDIA_CLASS_APP = "app" +MEDIA_CLASS_APPS = "apps" +MEDIA_CLASS_ARTIST = "artist" +MEDIA_CLASS_CHANNEL = "channel" +MEDIA_CLASS_CHANNELS = "channels" +MEDIA_CLASS_COMPOSER = "composer" +MEDIA_CLASS_CONTRIBUTING_ARTIST = "contributing_artist" +MEDIA_CLASS_DIRECTORY = "directory" +MEDIA_CLASS_EPISODE = "episode" +MEDIA_CLASS_GAME = "game" +MEDIA_CLASS_GENRE = "genre" +MEDIA_CLASS_IMAGE = "image" +MEDIA_CLASS_MOVIE = "movie" +MEDIA_CLASS_MUSIC = "music" +MEDIA_CLASS_PLAYLIST = "playlist" +MEDIA_CLASS_PODCAST = "podcast" +MEDIA_CLASS_SEASON = "season" +MEDIA_CLASS_TRACK = "track" +MEDIA_CLASS_TV_SHOW = "tv_show" +MEDIA_CLASS_URL = "url" +MEDIA_CLASS_VIDEO = "video" + MEDIA_TYPE_ALBUM = "album" MEDIA_TYPE_APP = "app" MEDIA_TYPE_APPS = "apps" diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index d670cee1676..774ee64d852 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -6,6 +6,7 @@ from typing import Tuple from aiohttp import web from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant, callback @@ -114,6 +115,7 @@ class LocalSource(MediaSource): media = BrowseMediaSource( domain=DOMAIN, identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="directory", title=title, can_play=is_file, diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index cd8e44f4a24..5d768fd79d8 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -5,6 +5,8 @@ from typing import List, Optional, Tuple from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, ) @@ -52,6 +54,7 @@ class MediaSourceItem: base = BrowseMediaSource( domain=None, identifier=None, + media_class=MEDIA_CLASS_CHANNELS, media_content_type=MEDIA_TYPE_CHANNELS, title="Media Sources", can_play=False, @@ -61,6 +64,7 @@ class MediaSourceItem: BrowseMediaSource( domain=source.domain, identifier=None, + media_class=MEDIA_CLASS_CHANNEL, media_content_type=MEDIA_TYPE_CHANNEL, title=source.name, can_play=False, diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 09c34ec41af..02ffd608472 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -4,7 +4,10 @@ import logging import re from typing import Optional, Tuple -from homeassistant.components.media_player.const import MEDIA_TYPE_VIDEO +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_VIDEO, + MEDIA_TYPE_VIDEO, +) from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.const import MEDIA_MIME_TYPES from homeassistant.components.media_source.error import MediaSourceError, Unresolvable @@ -91,6 +94,7 @@ class NetatmoSource(MediaSource): media = BrowseMediaSource( domain=DOMAIN, identifier=path, + media_class=MEDIA_CLASS_VIDEO, media_content_type=MEDIA_TYPE_VIDEO, title=title, can_play=bool( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 0e2d3f97c49..a780fe6b635 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -11,6 +11,8 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, @@ -288,6 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): return BrowseMedia( title="Channels", + media_class=MEDIA_CLASS_CHANNELS, media_content_id="", media_content_type=MEDIA_TYPE_CHANNELS, can_play=False, @@ -295,6 +298,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): children=[ BrowseMedia( title=channel, + media_class=MEDIA_CLASS_CHANNEL, media_content_id=channel, media_content_type=MEDIA_TYPE_CHANNEL, can_play=True, diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 1d3f3616450..9d5572a4faa 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -2,13 +2,31 @@ import logging from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_EPISODE, + MEDIA_CLASS_MOVIE, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_SEASON, + MEDIA_CLASS_TRACK, + MEDIA_CLASS_TV_SHOW, + MEDIA_CLASS_VIDEO, +) from homeassistant.components.media_player.errors import BrowseError from .const import DOMAIN + +class UnknownMediaType(BrowseError): + """Unknown media type.""" + + EXPANDABLES = ["album", "artist", "playlist", "season", "show"] PLAYLISTS_BROWSE_PAYLOAD = { "title": "Playlists", + "media_class": MEDIA_CLASS_PLAYLIST, "media_content_id": "all", "media_content_type": "playlists", "can_play": False, @@ -19,6 +37,18 @@ SPECIAL_METHODS = { "Recently Added": "recentlyAdded", } +ITEM_TYPE_MEDIA_CLASS = { + "album": MEDIA_CLASS_ALBUM, + "artist": MEDIA_CLASS_ARTIST, + "episode": MEDIA_CLASS_EPISODE, + "movie": MEDIA_CLASS_MOVIE, + "playlist": MEDIA_CLASS_PLAYLIST, + "season": MEDIA_CLASS_SEASON, + "show": MEDIA_CLASS_TV_SHOW, + "track": MEDIA_CLASS_TRACK, + "video": MEDIA_CLASS_VIDEO, +} + _LOGGER = logging.getLogger(__name__) @@ -34,11 +64,17 @@ def browse_media( if media is None: return None - media_info = item_payload(media) + try: + media_info = item_payload(media) + except UnknownMediaType: + return None if media_info.can_expand: media_info.children = [] for item in media: - media_info.children.append(item_payload(item)) + try: + media_info.children.append(item_payload(item)) + except UnknownMediaType: + continue return media_info if media_content_id and ":" in media_content_id: @@ -65,6 +101,7 @@ def browse_media( payload = { "title": title, + "media_class": MEDIA_CLASS_DIRECTORY, "media_content_id": f"{media_content_id}:{special_folder}", "media_content_type": media_content_type, "can_play": False, @@ -75,7 +112,10 @@ def browse_media( method = SPECIAL_METHODS[special_folder] items = getattr(library_or_section, method)() for item in items: - payload["children"].append(item_payload(item)) + try: + payload["children"].append(item_payload(item)) + except UnknownMediaType: + continue return BrowseMedia(**payload) if media_content_type in ["server", None]: @@ -99,8 +139,14 @@ def browse_media( def item_payload(item): """Create response payload for a single media item.""" + try: + media_class = ITEM_TYPE_MEDIA_CLASS[item.type] + except KeyError as err: + _LOGGER.debug("Unknown type received: %s", item.type) + raise UnknownMediaType from err payload = { "title": item.title, + "media_class": media_class, "media_content_id": str(item.ratingKey), "media_content_type": item.type, "can_play": True, @@ -116,6 +162,7 @@ def library_section_payload(section): """Create response payload for a single library section.""" return BrowseMedia( title=section.title, + media_class=MEDIA_CLASS_DIRECTORY, media_content_id=section.key, media_content_type="library", can_play=False, @@ -128,6 +175,7 @@ def special_library_payload(parent_payload, special_type): title = f"{special_type} ({parent_payload.title})" return BrowseMedia( title=title, + media_class=parent_payload.media_class, media_content_id=f"{parent_payload.media_content_id}:{special_type}", media_content_type=parent_payload.media_content_type, can_play=False, @@ -139,6 +187,7 @@ def server_payload(plex_server): """Create response payload to describe libraries of the Plex server.""" server_info = BrowseMedia( title=plex_server.friendly_name, + media_class=MEDIA_CLASS_DIRECTORY, media_content_id=plex_server.machine_identifier, media_content_type="server", can_play=False, @@ -165,7 +214,10 @@ def library_payload(plex_server, library_id): special_library_payload(library_info, "Recently Added") ) for item in library.all(): - library_info.children.append(item_payload(item)) + try: + library_info.children.append(item_payload(item)) + except UnknownMediaType: + continue return library_info @@ -173,5 +225,8 @@ def playlists_payload(plex_server): """Create response payload for all available playlists.""" playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []} for playlist in plex_server.playlists(): - playlists_info["children"].append(item_payload(playlist)) + try: + playlists_info["children"].append(item_payload(playlist)) + except UnknownMediaType: + continue return BrowseMedia(**playlists_info) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index c8f1b6d999d..7935206f114 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -11,6 +11,11 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( + MEDIA_CLASS_APP, + MEDIA_CLASS_APPS, + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_CHANNELS, + MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_APP, MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, @@ -79,6 +84,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: """Create response payload to describe contents of a specific library.""" library_info = BrowseMedia( title="Media Library", + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", media_content_type="library", can_play=False, @@ -89,6 +95,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Apps", + media_class=MEDIA_CLASS_APPS, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -100,6 +107,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Channels", + media_class=MEDIA_CLASS_CHANNELS, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, @@ -286,6 +294,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_APPS: response = BrowseMedia( title="Apps", + media_class=MEDIA_CLASS_APPS, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -294,6 +303,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): BrowseMedia( title=app.name, thumbnail=self.coordinator.roku.app_icon_url(app.app_id), + media_class=MEDIA_CLASS_APP, media_content_id=app.app_id, media_content_type=MEDIA_TYPE_APP, can_play=True, @@ -306,6 +316,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_CHANNELS: response = BrowseMedia( title="Channels", + media_class=MEDIA_CLASS_CHANNELS, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, @@ -313,6 +324,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): children=[ BrowseMedia( title=channel.name, + media_class=MEDIA_CLASS_CHANNEL, media_content_id=channel.number, media_content_type=MEDIA_TYPE_CHANNEL, can_play=True, diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 47610f242eb..9a245f8d4d7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -17,6 +17,14 @@ import voluptuous as vol from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_COMPOSER, + MEDIA_CLASS_CONTRIBUTING_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_GENRE, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_TRACK, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_COMPOSER, @@ -103,6 +111,23 @@ EXPANDABLE_MEDIA_TYPES = [ SONOS_PLAYLISTS, ] +SONOS_TO_MEDIA_CLASSES = { + SONOS_ALBUM: MEDIA_CLASS_ALBUM, + SONOS_ALBUM_ARTIST: MEDIA_CLASS_ARTIST, + SONOS_ARTIST: MEDIA_CLASS_CONTRIBUTING_ARTIST, + SONOS_COMPOSER: MEDIA_CLASS_COMPOSER, + SONOS_GENRE: MEDIA_CLASS_GENRE, + SONOS_PLAYLISTS: MEDIA_CLASS_PLAYLIST, + SONOS_TRACKS: MEDIA_CLASS_TRACK, + "object.container.album.musicAlbum": MEDIA_CLASS_ALBUM, + "object.container.genre.musicGenre": MEDIA_CLASS_PLAYLIST, + "object.container.person.composer": MEDIA_CLASS_PLAYLIST, + "object.container.person.musicArtist": MEDIA_CLASS_ARTIST, + "object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST, + "object.container.playlistContainer": MEDIA_CLASS_PLAYLIST, + "object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK, +} + SONOS_TO_MEDIA_TYPES = { SONOS_ALBUM: MEDIA_TYPE_ALBUM, SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST, @@ -1462,9 +1487,12 @@ def build_item_response(media_library, payload): except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] + media_class = SONOS_TO_MEDIA_CLASSES[MEDIA_TYPES_TO_SONOS[payload["search_type"]]] + return BrowseMedia( title=title, thumbnail=thumbnail, + media_class=media_class, media_content_id=payload["idstring"], media_content_type=payload["search_type"], children=[item_payload(item) for item in media], @@ -1482,6 +1510,7 @@ def item_payload(item): return BrowseMedia( title=item.title, thumbnail=getattr(item, "album_art_uri", None), + media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)], media_content_id=get_content_id(item), media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)], can_play=can_play(item.item_class), @@ -1497,6 +1526,7 @@ def library_payload(media_library): """ return BrowseMedia( title="Music Library", + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", media_content_type="library", can_play=False, diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index cc62cb9e276..91e0c85aa3c 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -11,6 +11,12 @@ from yarl import URL from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player.const import ( + MEDIA_CLASS_ALBUM, + MEDIA_CLASS_ARTIST, + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_PLAYLIST, + MEDIA_CLASS_PODCAST, + MEDIA_CLASS_TRACK, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, MEDIA_TYPE_EPISODE, @@ -96,6 +102,29 @@ LIBRARY_MAP = { "new_releases": "New Releases", } +CONTENT_TYPE_MEDIA_CLASS = { + "current_user_playlists": MEDIA_CLASS_PLAYLIST, + "current_user_followed_artists": MEDIA_CLASS_ARTIST, + "current_user_saved_albums": MEDIA_CLASS_ALBUM, + "current_user_saved_tracks": MEDIA_CLASS_TRACK, + "current_user_saved_shows": MEDIA_CLASS_PODCAST, + "current_user_recently_played": MEDIA_CLASS_TRACK, + "current_user_top_artists": MEDIA_CLASS_ARTIST, + "current_user_top_tracks": MEDIA_CLASS_TRACK, + "featured_playlists": MEDIA_CLASS_PLAYLIST, + "categories": MEDIA_CLASS_DIRECTORY, + "category_playlists": MEDIA_CLASS_PLAYLIST, + "new_releases": MEDIA_CLASS_ALBUM, + MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, + MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST, +} + + +class MissingMediaInformation(BrowseError): + """Missing media required information.""" + async def async_setup_entry( hass: HomeAssistant, @@ -498,24 +527,32 @@ def build_item_response(spotify, user, payload): return None if media_content_type == "categories": - return BrowseMedia( + media_item = BrowseMedia( title=LIBRARY_MAP.get(media_content_id), + media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type], media_content_id=media_content_id, media_content_type=media_content_type, can_play=False, can_expand=True, - children=[ + children=[], + ) + for item in items: + try: + item_id = item["id"] + except KeyError: + _LOGGER.debug("Missing id for media item: %s", item) + continue + media_item.children.append( BrowseMedia( title=item.get("name"), - media_content_id=item["id"], + media_class=MEDIA_CLASS_PLAYLIST, + media_content_id=item_id, media_content_type="category_playlists", thumbnail=fetch_image_url(item, key="icons"), can_play=False, can_expand=True, ) - for item in items - ], - ) + ) if title is None: if "name" in media: @@ -525,12 +562,18 @@ def build_item_response(spotify, user, payload): response = { "title": title, + "media_class": CONTENT_TYPE_MEDIA_CLASS[media_content_type], "media_content_id": media_content_id, "media_content_type": media_content_type, "can_play": media_content_type in PLAYABLE_MEDIA_TYPES, - "children": [item_payload(item) for item in items], + "children": [], "can_expand": True, } + for item in items: + try: + response["children"].append(item_payload(item)) + except MissingMediaInformation: + continue if "images" in media: response["thumbnail"] = fetch_image_url(media) @@ -546,20 +589,31 @@ def item_payload(item): Used by async_browse_media. """ + try: + media_type = item["type"] + media_id = item["uri"] + except KeyError as err: + _LOGGER.debug("Missing type or uri for media item: %s", item) + raise MissingMediaInformation from err - can_expand = item["type"] not in [ + can_expand = media_type not in [ MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, ] payload = { "title": item.get("name"), - "media_content_id": item["uri"], - "media_content_type": item["type"], - "can_play": item["type"] in PLAYABLE_MEDIA_TYPES, + "media_content_id": media_id, + "media_content_type": media_type, + "can_play": media_type in PLAYABLE_MEDIA_TYPES, "can_expand": can_expand, } + payload = { + **payload, + "media_class": CONTENT_TYPE_MEDIA_CLASS[media_type], + } + if "images" in item: payload["thumbnail"] = fetch_image_url(item) elif MEDIA_TYPE_ALBUM in item: @@ -576,6 +630,7 @@ def library_payload(): """ library_info = { "title": "Media Library", + "media_class": MEDIA_CLASS_DIRECTORY, "media_content_id": "library", "media_content_type": "library", "can_play": False, diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index eb387fcc6a3..68e0fcda1d8 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components import media_source +from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const from homeassistant.setup import async_setup_component @@ -77,6 +78,7 @@ async def test_websocket_browse_media(hass, hass_ws_client): domain=const.DOMAIN, identifier="/media", title="Local Media", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="listing", can_play=False, can_expand=True, diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py index f951fcfb0c0..3d19edd722d 100644 --- a/tests/components/media_source/test_models.py +++ b/tests/components/media_source/test_models.py @@ -1,5 +1,9 @@ """Test Media Source model methods.""" -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MUSIC, + MEDIA_TYPE_MUSIC, +) from homeassistant.components.media_source import const, models @@ -8,6 +12,7 @@ async def test_browse_media_as_dict(): base = models.BrowseMediaSource( domain=const.DOMAIN, identifier="media", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="folder", title="media/", can_play=False, @@ -17,6 +22,7 @@ async def test_browse_media_as_dict(): models.BrowseMediaSource( domain=const.DOMAIN, identifier="media/test.mp3", + media_class=MEDIA_CLASS_MUSIC, media_content_type=MEDIA_TYPE_MUSIC, title="test.mp3", can_play=True, @@ -26,12 +32,14 @@ async def test_browse_media_as_dict(): item = base.as_dict() assert item["title"] == "media/" + assert item["media_class"] == MEDIA_CLASS_DIRECTORY assert item["media_content_type"] == "folder" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"] assert item["can_expand"] assert len(item["children"]) == 1 assert item["children"][0]["title"] == "test.mp3" + assert item["children"][0]["media_class"] == MEDIA_CLASS_MUSIC async def test_browse_media_parent_no_children(): @@ -39,6 +47,7 @@ async def test_browse_media_parent_no_children(): base = models.BrowseMediaSource( domain=const.DOMAIN, identifier="media", + media_class=MEDIA_CLASS_DIRECTORY, media_content_type="folder", title="media/", can_play=False, @@ -47,6 +56,7 @@ async def test_browse_media_parent_no_children(): item = base.as_dict() assert item["title"] == "media/" + assert item["media_class"] == MEDIA_CLASS_DIRECTORY assert item["media_content_type"] == "folder" assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"] From c9ec533aa5595ff9a616bc51a14c91b9dc67f3da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 02:41:17 -0500 Subject: [PATCH 741/862] Add the ability to reload bayesian platforms from yaml (#39771) --- homeassistant/components/bayesian/__init__.py | 3 + .../components/bayesian/binary_sensor.py | 5 ++ .../components/bayesian/services.yaml | 2 + .../components/bayesian/test_binary_sensor.py | 59 ++++++++++++++++++- tests/fixtures/bayesian/configuration.yaml | 10 ++++ 5 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bayesian/services.yaml create mode 100644 tests/fixtures/bayesian/configuration.yaml diff --git a/homeassistant/components/bayesian/__init__.py b/homeassistant/components/bayesian/__init__.py index 971ff8427ac..485592dc5e4 100644 --- a/homeassistant/components/bayesian/__init__.py +++ b/homeassistant/components/bayesian/__init__.py @@ -1 +1,4 @@ """The bayesian component.""" + +DOMAIN = "bayesian" +PLATFORMS = ["binary_sensor"] diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 8d4dab62263..90540e456c5 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -25,8 +25,11 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_template_result, ) +from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.template import result_as_boolean +from . import DOMAIN, PLATFORMS + ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" ATTR_PROBABILITY = "probability" @@ -106,6 +109,8 @@ def update_probability(prior, prob_given_true, prob_given_false): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Bayesian Binary sensor.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + name = config[CONF_NAME] observations = config[CONF_OBSERVATIONS] prior = config[CONF_PRIOR] diff --git a/homeassistant/components/bayesian/services.yaml b/homeassistant/components/bayesian/services.yaml new file mode 100644 index 00000000000..ec7313a8630 --- /dev/null +++ b/homeassistant/components/bayesian/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload all bayesian entities. diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 9e4983ab4d5..57c7f404c7f 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -1,15 +1,18 @@ """The test for the bayesian sensor platform.""" import json +from os import path import unittest -from homeassistant.components.bayesian import binary_sensor as bayesian +from homeassistant import config as hass_config +from homeassistant.components.bayesian import DOMAIN, binary_sensor as bayesian from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_RELOAD, STATE_UNKNOWN from homeassistant.setup import async_setup_component, setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -631,3 +634,55 @@ async def test_monitored_sensor_goes_away(hass): await hass.async_block_till_done() assert hass.states.get("binary_sensor.test_binary").state == "on" + + +async def test_reload(hass): + """Verify we can reload bayesian sensors.""" + + config = { + "binary_sensor": { + "name": "test", + "platform": "bayesian", + "observations": [ + { + "platform": "state", + "entity_id": "sensor.test_monitored", + "to_state": "on", + "prob_given_true": 0.9, + "prob_given_false": 0.4, + }, + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("binary_sensor.test") + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "bayesian/configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + assert hass.states.get("binary_sensor.test") is None + assert hass.states.get("binary_sensor.test2") + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) diff --git a/tests/fixtures/bayesian/configuration.yaml b/tests/fixtures/bayesian/configuration.yaml new file mode 100644 index 00000000000..56a490d4aec --- /dev/null +++ b/tests/fixtures/bayesian/configuration.yaml @@ -0,0 +1,10 @@ +binary_sensor: + - platform: bayesian + prior: 0.1 + observations: + - entity_id: 'switch.kitchen_lights' + prob_given_true: 0.6 + prob_given_false: 0.2 + platform: 'state' + to_state: 'on' + name: test2 From f34e831650a0a5cce65774f50fd3f6fe6cf47eea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Sep 2020 11:59:39 +0200 Subject: [PATCH 742/862] Remove invalidation version from panel_custom (#39782) --- homeassistant/components/panel_custom/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index e663665675c..a2175253ce1 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -50,7 +50,7 @@ CONFIG_SCHEMA = vol.Schema( cv.ensure_list, [ vol.All( - cv.deprecated(CONF_WEBCOMPONENT_PATH, invalidation_version="0.115"), + cv.deprecated(CONF_WEBCOMPONENT_PATH), vol.Schema( { vol.Required(CONF_COMPONENT_NAME): cv.string, From f41d28335447c75dc0a548e234bd1217ebe71318 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 8 Sep 2020 13:50:53 +0200 Subject: [PATCH 743/862] Restore miflora now that v0.7.0 is out (#39787) * add miflora again, reverts part of github.com/home-assistant/core/pull/37707 * edit CODEOWNERS --- CODEOWNERS | 2 +- homeassistant/components/miflora/manifest.json | 5 ++--- requirements_all.txt | 4 ++++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6230904de7b..70d89c5e45e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -253,7 +253,7 @@ homeassistant/components/met/* @danielhiversen @thimic homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/metoffice/* @MrHarcombe -homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel +homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel @basnijholt homeassistant/components/mikrotik/* @engrbm87 homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index 96558c82fec..7795e8fb6f8 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -1,8 +1,7 @@ { - "disabled": "Dependency contains code that breaks Home Assistant.", "domain": "miflora", "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", - "requirements": ["bluepy==1.3.0", "miflora==0.6.0"], - "codeowners": ["@danielhiversen", "@ChristianKuehnel"] + "requirements": ["bluepy==1.3.0", "miflora==0.7.0"], + "codeowners": ["@danielhiversen", "@ChristianKuehnel", "@basnijholt"] } diff --git a/requirements_all.txt b/requirements_all.txt index f0198702f88..272ef7bcc10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -363,6 +363,7 @@ blinkstick==1.1.8 blockchain==1.4.4 # homeassistant.components.decora +# homeassistant.components.miflora # bluepy==1.3.0 # homeassistant.components.bme680 @@ -917,6 +918,9 @@ meteofrance-api==0.1.1 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.miflora +miflora==0.7.0 + # homeassistant.components.mill millheater==0.3.4 From 02600bf1902309e945997d49dffffbb342993257 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 8 Sep 2020 14:13:48 +0200 Subject: [PATCH 744/862] Fix Sonos issue (#39790) --- homeassistant/components/sonos/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 9a245f8d4d7..51287f9f288 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1595,6 +1595,7 @@ def get_media(media_library, item_id, search_type): search_type, "/".join(item_id.split("/")[:-1]), full_album_art_uri=True, + max_items=0, ): if item.item_id == item_id: return item From 7f801faed107dc22a1b469a6cd88cddaa16179b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:23:38 +0200 Subject: [PATCH 745/862] Copy instead of deepcopy the variables in a wait for trigger (#39796) --- homeassistant/helpers/script.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 604102e6af3..74660f8b391 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,6 +1,5 @@ """Helpers to execute scripts.""" import asyncio -from copy import deepcopy from datetime import datetime, timedelta from functools import partial import itertools @@ -572,7 +571,7 @@ class _ScriptRun: "" if delay is None else f" (timeout: {timedelta(seconds=delay)})", ) - variables = deepcopy(self._variables) + variables = {**self._variables} self._variables["wait"] = {"remaining": delay, "trigger": None} async def async_done(variables, context=None): From 2a68952334386f021cae4b933bbfc30425c94f76 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:12:20 +0200 Subject: [PATCH 746/862] Some shelly fixes (#39798) --- .coveragerc | 1 - homeassistant/components/shelly/__init__.py | 2 +- homeassistant/components/shelly/manifest.json | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index fe7a8d74cbd..1a46607e567 100644 --- a/.coveragerc +++ b/.coveragerc @@ -762,7 +762,6 @@ omit = homeassistant/components/shelly/binary_sensor.py homeassistant/components/shelly/cover.py homeassistant/components/shelly/entity.py - homeassistant/components/shelly/cover.py homeassistant/components/shelly/light.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/switch.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 39fc34d6d56..83d5d7b9f3a 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers import aiohttp_client, device_registry, update_coordi from .const import DOMAIN -PLATFORMS = ["binary_sensor", "light", "sensor", "switch", "cover"] +PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 010ecf16a56..357f2c10fda 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -2,7 +2,7 @@ "domain": "shelly", "name": "Shelly", "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/shelly2", + "documentation": "https://www.home-assistant.io/integrations/shelly", "requirements": ["aioshelly==0.3.0"], "zeroconf": ["_http._tcp.local."], "codeowners": ["@balloob", "@bieniu"] From c6a7350db1f274d542961c5c28ddffabc62d50f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:42:50 +0200 Subject: [PATCH 747/862] Remove HTML support from frontend (#39799) --- homeassistant/components/frontend/__init__.py | 67 ++++------- .../components/panel_custom/__init__.py | 106 ++++-------------- tests/components/frontend/test_init.py | 8 -- tests/components/panel_custom/test_init.py | 71 ------------ 4 files changed, 48 insertions(+), 204 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2a741afcfbc..73eb24d08cd 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -70,8 +70,6 @@ MANIFEST_JSON = { DATA_PANELS = "frontend_panels" DATA_JS_VERSION = "frontend_js_version" -DATA_EXTRA_HTML_URL = "frontend_extra_html_url" -DATA_EXTRA_HTML_URL_ES5 = "frontend_extra_html_url_es5" DATA_EXTRA_MODULE_URL = "frontend_extra_module_url" DATA_EXTRA_JS_URL_ES5 = "frontend_extra_js_url_es5" @@ -91,29 +89,23 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.All( - cv.deprecated(CONF_EXTRA_HTML_URL, invalidation_version="0.115"), - cv.deprecated(CONF_EXTRA_HTML_URL_ES5, invalidation_version="0.115"), - vol.Schema( - { - vol.Optional(CONF_FRONTEND_REPO): cv.isdir, - vol.Optional(CONF_THEMES): vol.Schema( - {cv.string: {cv.string: cv.string}} - ), - vol.Optional(CONF_EXTRA_HTML_URL): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_EXTRA_MODULE_URL): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_EXTRA_JS_URL_ES5): vol.All( - cv.ensure_list, [cv.string] - ), - # We no longer use these options. - vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all, - vol.Optional(CONF_JS_VERSION): cv.match_all, - }, - ), + DOMAIN: vol.Schema( + { + vol.Optional(CONF_FRONTEND_REPO): cv.isdir, + vol.Optional(CONF_THEMES): vol.Schema( + {cv.string: {cv.string: cv.string}} + ), + vol.Optional(CONF_EXTRA_MODULE_URL): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_EXTRA_JS_URL_ES5): vol.All( + cv.ensure_list, [cv.string] + ), + # We no longer use these options. + vol.Optional(CONF_EXTRA_HTML_URL): cv.match_all, + vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all, + vol.Optional(CONF_JS_VERSION): cv.match_all, + }, ) }, extra=vol.ALLOW_EXTRA, @@ -220,17 +212,6 @@ def async_remove_panel(hass, frontend_url_path): hass.bus.async_fire(EVENT_PANELS_UPDATED) -@bind_hass -@callback -def add_extra_html_url(hass, url, es5=False): - """Register extra html url to load.""" - key = DATA_EXTRA_HTML_URL_ES5 if es5 else DATA_EXTRA_HTML_URL - url_set = hass.data.get(key) - if url_set is None: - url_set = hass.data[key] = set() - url_set.add(url) - - def add_extra_js_url(hass, url, es5=False): """Register extra js or module url to load.""" key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL @@ -267,6 +248,13 @@ async def async_setup(hass, config): conf = config.get(DOMAIN, {}) + for key in (CONF_EXTRA_HTML_URL, CONF_EXTRA_HTML_URL_ES5, CONF_JS_VERSION): + if key in conf: + _LOGGER.error( + "Please remove %s from your frontend config. It is no longer supported", + key, + ) + repo_path = conf.get(CONF_FRONTEND_REPO) is_dev = repo_path is not None root_path = _frontend_root(repo_path) @@ -315,12 +303,6 @@ async def async_setup(hass, config): sidebar_icon="hass:hammer", ) - if DATA_EXTRA_HTML_URL not in hass.data: - hass.data[DATA_EXTRA_HTML_URL] = set() - - for url in conf.get(CONF_EXTRA_HTML_URL, []): - add_extra_html_url(hass, url, False) - if DATA_EXTRA_MODULE_URL not in hass.data: hass.data[DATA_EXTRA_MODULE_URL] = set() @@ -522,7 +504,6 @@ class IndexView(web_urldispatcher.AbstractResource): return web.Response( text=template.render( theme_color=MANIFEST_JSON["theme_color"], - extra_urls=hass.data[DATA_EXTRA_HTML_URL], extra_modules=hass.data[DATA_EXTRA_MODULE_URL], extra_js_es5=hass.data[DATA_EXTRA_JS_URL_ES5], ), diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index a2175253ce1..8bab4d019e6 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -1,6 +1,5 @@ """Register a custom front end panel.""" import logging -import os import voluptuous as vol @@ -15,7 +14,6 @@ CONF_SIDEBAR_TITLE = "sidebar_title" CONF_SIDEBAR_ICON = "sidebar_icon" CONF_URL_PATH = "url_path" CONF_CONFIG = "config" -CONF_WEBCOMPONENT_PATH = "webcomponent_path" CONF_JS_URL = "js_url" CONF_MODULE_URL = "module_url" CONF_EMBED_IFRAME = "embed_iframe" @@ -32,55 +30,34 @@ LEGACY_URL = "/api/panel_custom/{}" PANEL_DIR = "panels" -def url_validator(value): - """Validate required urls are specified.""" - has_js_url = CONF_JS_URL in value - has_html_url = CONF_WEBCOMPONENT_PATH in value - has_module_url = CONF_MODULE_URL in value - - if has_html_url and (has_js_url or has_module_url): - raise vol.Invalid("You cannot specify other urls besides a webcomponent path") - - return value - - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( cv.ensure_list, [ - vol.All( - cv.deprecated(CONF_WEBCOMPONENT_PATH), - vol.Schema( - { - vol.Required(CONF_COMPONENT_NAME): cv.string, - vol.Optional(CONF_SIDEBAR_TITLE): cv.string, - vol.Optional( - CONF_SIDEBAR_ICON, default=DEFAULT_ICON - ): cv.icon, - vol.Optional(CONF_URL_PATH): cv.string, - vol.Optional(CONF_CONFIG): dict, - vol.Optional( - CONF_WEBCOMPONENT_PATH, - ): cv.string, - vol.Optional( - CONF_JS_URL, - ): cv.string, - vol.Optional( - CONF_MODULE_URL, - ): cv.string, - vol.Optional( - CONF_EMBED_IFRAME, default=DEFAULT_EMBED_IFRAME - ): cv.boolean, - vol.Optional( - CONF_TRUST_EXTERNAL_SCRIPT, - default=DEFAULT_TRUST_EXTERNAL, - ): cv.boolean, - vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean, - } - ), - url_validator, - ) + vol.Schema( + { + vol.Required(CONF_COMPONENT_NAME): cv.string, + vol.Optional(CONF_SIDEBAR_TITLE): cv.string, + vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon, + vol.Optional(CONF_URL_PATH): cv.string, + vol.Optional(CONF_CONFIG): dict, + vol.Optional( + CONF_JS_URL, + ): cv.string, + vol.Optional( + CONF_MODULE_URL, + ): cv.string, + vol.Optional( + CONF_EMBED_IFRAME, default=DEFAULT_EMBED_IFRAME + ): cv.boolean, + vol.Optional( + CONF_TRUST_EXTERNAL_SCRIPT, + default=DEFAULT_TRUST_EXTERNAL, + ): cv.boolean, + vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean, + } + ), ], ) }, @@ -98,8 +75,6 @@ async def async_register_panel( # Title/icon for sidebar sidebar_title=None, sidebar_icon=None, - # HTML source of your panel - html_url=None, # JS source of your panel js_url=None, # JS module of your panel @@ -114,16 +89,11 @@ async def async_register_panel( require_admin=False, ): """Register a new custom panel.""" - if js_url is None and html_url is None and module_url is None: + if js_url is None and module_url is None: raise ValueError("Either js_url, module_url or html_url is required.") - if html_url and (js_url or module_url): - raise ValueError("You cannot specify other paths with an HTML url") if config is not None and not isinstance(config, dict): raise ValueError("Config needs to be a dictionary.") - if html_url: - _LOGGER.warning("HTML custom panels have been deprecated") - custom_panel_config = { "name": webcomponent_name, "embed_iframe": embed_iframe, @@ -136,9 +106,6 @@ async def async_register_panel( if module_url is not None: custom_panel_config["module_url"] = module_url - if html_url is not None: - custom_panel_config["html_url"] = html_url - if config is not None: # Make copy because we're mutating it config = dict(config) @@ -162,8 +129,6 @@ async def async_setup(hass, config): if DOMAIN not in config: return True - seen = set() - for panel in config[DOMAIN]: name = panel[CONF_COMPONENT_NAME] @@ -184,29 +149,6 @@ async def async_setup(hass, config): if CONF_MODULE_URL in panel: kwargs["module_url"] = panel[CONF_MODULE_URL] - if CONF_MODULE_URL not in panel and CONF_JS_URL not in panel: - if name in seen: - _LOGGER.warning( - "Got HTML panel with duplicate name %s. Not registering", name - ) - continue - - seen.add(name) - panel_path = panel.get(CONF_WEBCOMPONENT_PATH) - - if panel_path is None: - panel_path = hass.config.path(PANEL_DIR, f"{name}.html") - - if not await hass.async_add_executor_job(os.path.isfile, panel_path): - _LOGGER.error( - "Unable to find webcomponent for %s: %s", name, panel_path - ) - continue - - url = LEGACY_URL.format(name) - hass.http.register_static_path(url, panel_path) - kwargs["html_url"] = url - try: await async_register_panel(hass, **kwargs) except ValueError as err: diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index a7ecbb0e5fe..7298ad753d2 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -334,14 +334,6 @@ async def test_missing_themes(hass, hass_ws_client): assert msg["result"]["themes"] == {} -async def test_extra_urls(mock_http_client_with_urls, mock_onboarded): - """Test that extra urls are loaded.""" - resp = await mock_http_client_with_urls.get("/lovelace?latest") - assert resp.status == 200 - text = await resp.text() - assert text.find('href="https://domain.com/my_extra_url.html"') >= 0 - - async def test_get_panels(hass, hass_ws_client, mock_http_client): """Test get_panels command.""" events = async_capture_events(hass, EVENT_PANELS_UPDATED) diff --git a/tests/components/panel_custom/test_init.py b/tests/components/panel_custom/test_init.py index caa55749c50..ddcb4079ef7 100644 --- a/tests/components/panel_custom/test_init.py +++ b/tests/components/panel_custom/test_init.py @@ -30,52 +30,6 @@ async def test_webcomponent_custom_path_not_found(hass): assert "nice_url" not in panels -async def test_webcomponent_custom_path(hass, caplog): - """Test if a web component is found in config panels dir.""" - filename = "mock.file" - - config = { - "panel_custom": [ - { - "name": "todo-mvc", - "webcomponent_path": filename, - "sidebar_title": "Sidebar Title", - "sidebar_icon": "mdi:iconicon", - "url_path": "nice_url", - "config": {"hello": "world"}, - }, - {"name": "todo-mvc"}, - ] - } - - with patch("os.path.isfile", Mock(return_value=True)): - with patch("os.access", Mock(return_value=True)): - result = await setup.async_setup_component(hass, "panel_custom", config) - assert result - - panels = hass.data.get(frontend.DATA_PANELS, []) - - assert panels - assert "nice_url" in panels - - panel = panels["nice_url"] - - assert panel.config == { - "hello": "world", - "_panel_custom": { - "html_url": "/api/panel_custom/todo-mvc", - "name": "todo-mvc", - "embed_iframe": False, - "trust_external": False, - }, - } - assert panel.frontend_url_path == "nice_url" - assert panel.sidebar_icon == "mdi:iconicon" - assert panel.sidebar_title == "Sidebar Title" - - assert "Got HTML panel with duplicate name todo-mvc. Not registering" in caplog.text - - async def test_js_webcomponent(hass): """Test if a web component is found in config panels dir.""" config = { @@ -188,31 +142,6 @@ async def test_latest_and_es5_build(hass): assert panel.frontend_url_path == "nice_url" -async def test_url_option_conflict(hass): - """Test config with multiple url options.""" - to_try = [ - { - "panel_custom": { - "name": "todo-mvc", - "webcomponent_path": "/local/bla.html", - "js_url": "/local/bla.js", - } - }, - { - "panel_custom": { - "name": "todo-mvc", - "webcomponent_path": "/local/bla.html", - "module_url": "/local/bla.js", - "js_url": "/local/bla.js", - } - }, - ] - - for config in to_try: - result = await setup.async_setup_component(hass, "panel_custom", config) - assert not result - - async def test_url_path_conflict(hass): """Test config with overlapping url path.""" assert await setup.async_setup_component( From 8dee5f4cf8155bdd4326ca286222f407753ee494 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 8 Sep 2020 15:52:04 +0200 Subject: [PATCH 748/862] Remove deprecated Hue configuration (#39800) --- homeassistant/components/hue/__init__.py | 98 +----------------- tests/components/hue/test_init.py | 124 ----------------------- 2 files changed, 1 insertion(+), 221 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index a99f9dd8a2a..fb277ee7a67 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -1,14 +1,11 @@ """Support for the Philips Hue system.""" -import ipaddress import logging from aiohue.util import normalize_bridge_id -import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import persistent_notification -from homeassistant.const import CONF_HOST -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import device_registry as dr from .bridge import HueBridge from .const import ( @@ -21,80 +18,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONF_BRIDGES = "bridges" - -DATA_CONFIGS = "hue_configs" - -PHUE_CONFIG_FILE = "phue.conf" - -BRIDGE_CONFIG_SCHEMA = vol.Schema( - { - # Validate as IP address and then convert back to a string. - vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), - vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean, - vol.Optional(CONF_ALLOW_HUE_GROUPS): cv.boolean, - vol.Optional("filename"): str, - } -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN, invalidation_version="0.115.0"), - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_BRIDGES): vol.All( - cv.ensure_list, - [BRIDGE_CONFIG_SCHEMA], - ) - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - async def async_setup(hass, config): """Set up the Hue platform.""" - conf = config.get(DOMAIN) - if conf is None: - conf = {} - hass.data[DOMAIN] = {} - hass.data[DATA_CONFIGS] = {} - - # User has not configured bridges - if CONF_BRIDGES not in conf: - return True - - bridges = conf[CONF_BRIDGES] - - configured_hosts = { - entry.data.get("host") for entry in hass.config_entries.async_entries(DOMAIN) - } - - for bridge_conf in bridges: - host = bridge_conf[CONF_HOST] - - # Store config in hass.data so the config entry can find it - hass.data[DATA_CONFIGS][host] = bridge_conf - - if host in configured_hosts: - continue - - # No existing config entry found, trigger link config flow. Because we're - # inside the setup of this component we'll have to use hass.async_add_job - # to avoid a deadlock: creating a config entry will set up the component - # but the setup would block till the entry is created! - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": bridge_conf[CONF_HOST]}, - ) - ) - return True @@ -102,8 +29,6 @@ async def async_setup_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): """Set up a bridge from a config entry.""" - host = entry.data["host"] - config = hass.data[DATA_CONFIGS].get(host) # Migrate allow_unreachable from config entry data to config entry options if ( @@ -133,27 +58,6 @@ async def async_setup_entry( data.pop(CONF_ALLOW_HUE_GROUPS) hass.config_entries.async_update_entry(entry, data=data, options=options) - # Overwrite from YAML configuration - if config is not None: - options = {} - if CONF_ALLOW_HUE_GROUPS in config and ( - CONF_ALLOW_HUE_GROUPS not in entry.options - or config[CONF_ALLOW_HUE_GROUPS] != entry.options[CONF_ALLOW_HUE_GROUPS] - ): - options[CONF_ALLOW_HUE_GROUPS] = config[CONF_ALLOW_HUE_GROUPS] - - if CONF_ALLOW_UNREACHABLE in config and ( - CONF_ALLOW_UNREACHABLE not in entry.options - or config[CONF_ALLOW_UNREACHABLE] != entry.options[CONF_ALLOW_UNREACHABLE] - ): - options[CONF_ALLOW_UNREACHABLE] = config[CONF_ALLOW_UNREACHABLE] - - if options: - hass.config_entries.async_update_entry( - entry, - options={**entry.options, **options}, - ) - bridge = HueBridge(hass, entry) if not await bridge.async_setup(): diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 1d6db3498f1..70a6c3b8756 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -31,130 +31,6 @@ async def test_setup_with_no_config(hass): assert hass.data[hue.DOMAIN] == {} -async def test_setup_defined_hosts_known_auth(hass): - """Test we don't initiate a config entry if config bridge is known.""" - MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) - - with patch.object(hue, "async_setup_entry", return_value=True): - assert ( - await async_setup_component( - hass, - hue.DOMAIN, - { - hue.DOMAIN: { - hue.CONF_BRIDGES: [ - { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - }, - {hue.CONF_HOST: "1.1.1.1"}, - ] - } - }, - ) - is True - ) - - # Flow started for discovered bridge - assert len(hass.config_entries.flow.async_progress()) == 1 - - # Config stored for domain. - assert hass.data[hue.DATA_CONFIGS] == { - "0.0.0.0": { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - }, - "1.1.1.1": {hue.CONF_HOST: "1.1.1.1"}, - } - - -async def test_setup_defined_hosts_no_known_auth(hass): - """Test we initiate config entry if config bridge is not known.""" - assert ( - await async_setup_component( - hass, - hue.DOMAIN, - { - hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - } - } - }, - ) - is True - ) - - # Flow started for discovered bridge - assert len(hass.config_entries.flow.async_progress()) == 1 - - # Config stored for domain. - assert hass.data[hue.DATA_CONFIGS] == { - "0.0.0.0": { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - } - } - - -async def test_config_passed_to_config_entry(hass): - """Test that configured options for a host are loaded via config entry.""" - entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) - entry.add_to_hass(hass) - mock_registry = Mock() - with patch.object(hue, "HueBridge") as mock_bridge, patch( - "homeassistant.helpers.device_registry.async_get_registry", - return_value=mock_registry, - ): - mock_bridge.return_value.async_setup = AsyncMock(return_value=True) - mock_bridge.return_value.api.config = Mock( - mac="mock-mac", - bridgeid="mock-bridgeid", - modelid="mock-modelid", - swversion="mock-swversion", - ) - # Can't set name via kwargs - mock_bridge.return_value.api.config.name = "mock-name" - assert ( - await async_setup_component( - hass, - hue.DOMAIN, - { - hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - } - } - }, - ) - is True - ) - - assert len(mock_bridge.mock_calls) == 2 - p_hass, p_entry = mock_bridge.mock_calls[0][1] - - assert p_hass is hass - assert p_entry is entry - - assert len(mock_registry.mock_calls) == 1 - assert mock_registry.mock_calls[0][2] == { - "config_entry_id": entry.entry_id, - "connections": {("mac", "mock-mac")}, - "identifiers": {("hue", "mock-bridgeid")}, - "manufacturer": "Signify", - "name": "mock-name", - "model": "mock-modelid", - "sw_version": "mock-swversion", - } - - async def test_unload_entry(hass, mock_bridge_setup): """Test being able to unload an entry.""" entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) From 0d27e10d7779f2254014d191faf6704164ea58a7 Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Tue, 8 Sep 2020 16:18:34 +0200 Subject: [PATCH 749/862] Bump pydelijn to 0.6.1 (#39802) --- homeassistant/components/delijn/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index e3ab4f29512..1de62e8df0f 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -3,5 +3,5 @@ "name": "De Lijn", "documentation": "https://www.home-assistant.io/integrations/delijn", "codeowners": ["@bollewolle", "@Emilv2"], - "requirements": ["pydelijn==0.6.0"] + "requirements": ["pydelijn==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 272ef7bcc10..1a3afbfbba0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1298,7 +1298,7 @@ pydanfossair==0.1.0 pydeconz==72 # homeassistant.components.delijn -pydelijn==0.6.0 +pydelijn==0.6.1 # homeassistant.components.dexcom pydexcom==0.2.0 From fa0778700756c648cd25beefc11642ee0d3b7953 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 10:08:31 -0500 Subject: [PATCH 750/862] Fix cover template entities honoring titlecase True/False (#39803) --- homeassistant/components/template/cover.py | 7 ++-- homeassistant/components/template/light.py | 2 +- tests/components/template/test_cover.py | 41 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index e0a1b2bc33c..dc9e5ead1d0 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -249,15 +249,16 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._position = None return - if result in _VALID_STATES: - if result in ("true", STATE_OPEN): + state = result.lower() + if state in _VALID_STATES: + if state in ("true", STATE_OPEN): self._position = 100 else: self._position = 0 else: _LOGGER.error( "Received invalid cover is_on state: %s. Expected: %s", - result, + state, ", ".join(_VALID_STATES), ) self._position = None diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 5f87301cce8..2b79846986c 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -412,7 +412,7 @@ class LightTemplate(TemplateEntity, LightEntity): self._available = True return - state = str(result).lower() + state = result.lower() if state in _VALID_STATES: self._state = state in ("true", STATE_ON) else: diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index d51d71648bf..5deb540782c 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -1079,3 +1079,44 @@ async def test_unique_id(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + + +async def test_state_gets_lowercased(hass): + """Test True/False is lowercased.""" + + hass.states.async_set("binary_sensor.garage_door_sensor", "off") + + await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "garage_door": { + "friendly_name": "Garage Door", + "value_template": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + }, + }, + }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 2 + + assert hass.states.get("cover.garage_door").state == STATE_OPEN + hass.states.async_set("binary_sensor.garage_door_sensor", "on") + await hass.async_block_till_done() + assert hass.states.get("cover.garage_door").state == STATE_CLOSED From f1de903fb5e884d5b1b040663daa28dcfa3ce1aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 10:12:23 -0500 Subject: [PATCH 751/862] Restore missing device_class to template binary_sensor (#39805) --- homeassistant/components/template/binary_sensor.py | 5 +++++ tests/components/template/test_binary_sensor.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index bd6cb55b880..2e50448c037 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -194,3 +194,8 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity): def is_on(self): """Return true if sensor is on.""" return self._state + + @property + def device_class(self): + """Return the sensor class of the binary sensor.""" + return self._device_class diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 7003b55c7ed..f9db8be37c6 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -6,6 +6,7 @@ from unittest import mock from homeassistant import setup from homeassistant.const import ( + ATTR_DEVICE_CLASS, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, @@ -411,7 +412,10 @@ async def test_available_without_availability_template(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + state = hass.states.get("binary_sensor.test") + + assert state.state != STATE_UNAVAILABLE + assert state.attributes[ATTR_DEVICE_CLASS] == "motion" async def test_availability_template(hass): @@ -443,7 +447,10 @@ async def test_availability_template(hass): hass.states.async_set("sensor.test_state", STATE_ON) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + state = hass.states.get("binary_sensor.test") + + assert state.state != STATE_UNAVAILABLE + assert state.attributes[ATTR_DEVICE_CLASS] == "motion" async def test_invalid_attribute_template(hass, caplog): From a5dec53e1bb881b652e10f3dc612680b45f44ccd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2020 10:31:08 -0500 Subject: [PATCH 752/862] Fix isy994 send_node_command (#39806) --- .../components/isy994/binary_sensor.py | 2 - homeassistant/components/isy994/climate.py | 2 - homeassistant/components/isy994/cover.py | 2 - homeassistant/components/isy994/fan.py | 2 - homeassistant/components/isy994/light.py | 3 +- homeassistant/components/isy994/lock.py | 2 - homeassistant/components/isy994/sensor.py | 2 - homeassistant/components/isy994/services.py | 46 +++++++++++-------- homeassistant/components/isy994/switch.py | 2 - homeassistant/helpers/entity_platform.py | 16 +++++++ homeassistant/helpers/reload.py | 10 +--- 11 files changed, 47 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index dc32fcef230..6355a9bcece 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -49,7 +49,6 @@ from .const import ( ) from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services DEVICE_PARENT_REQUIRED = [ DEVICE_CLASS_OPENING, @@ -172,7 +171,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, BINARY_SENSOR, devices) async_add_entities(devices) - async_setup_device_services(hass) def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str): diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 7dfd9a083d3..bb98c3d31bf 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -52,7 +52,6 @@ from .const import ( ) from .entity import ISYNodeEntity from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids -from .services import async_setup_device_services ISY_SUPPORTED_FEATURES = ( SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE @@ -73,7 +72,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, CLIMATE, entities) async_add_entities(entities) - async_setup_device_services(hass) class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 41273f61f01..51a26585778 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -24,7 +24,6 @@ from .const import ( ) from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services async def async_setup_entry( @@ -43,7 +42,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, COVER, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYCoverEntity(ISYNodeEntity, CoverEntity): diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 96aa2144b1c..384ff22403a 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -18,7 +18,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services VALUE_TO_STATE = { 0: SPEED_OFF, @@ -51,7 +50,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, FAN, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYFanEntity(ISYNodeEntity, FanEntity): diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 8d58a7f5796..7f94c68714d 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -20,7 +20,7 @@ from .const import ( ) from .entity import ISYNodeEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services, async_setup_light_services +from .services import async_setup_light_services ATTR_LAST_BRIGHTNESS = "last_brightness" @@ -41,7 +41,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, LIGHT, devices) async_add_entities(devices) - async_setup_device_services(hass) async_setup_light_services(hass) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index da50e4e704a..ceb26f3044c 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -10,7 +10,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services VALUE_TO_STATE = {0: False, 100: True} @@ -31,7 +30,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, LOCK, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYLockEntity(ISYNodeEntity, LockEntity): diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 8ae646b2791..b4eba10fd34 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -19,7 +19,6 @@ from .const import ( ) from .entity import ISYEntity, ISYNodeEntity from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids -from .services import async_setup_device_services async def async_setup_entry( @@ -40,7 +39,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, SENSOR, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYSensorEntity(ISYNodeEntity): diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index f59db1f5716..ee12cedbca4 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -13,9 +13,10 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) -from homeassistant.core import callback +from homeassistant.core import ServiceCall, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import async_get_platforms import homeassistant.helpers.entity_registry as er from homeassistant.helpers.typing import HomeAssistantType @@ -353,6 +354,30 @@ def async_setup_services(hass: HomeAssistantType): domain=DOMAIN, service=SERVICE_RELOAD, service_func=async_reload_config_entries ) + async def _async_send_raw_node_command(call: ServiceCall): + await hass.helpers.service.entity_service_call( + async_get_platforms(hass, DOMAIN), SERVICE_SEND_RAW_NODE_COMMAND, call + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SEND_RAW_NODE_COMMAND, + schema=cv.make_entity_service_schema(SERVICE_SEND_RAW_NODE_COMMAND_SCHEMA), + service_func=_async_send_raw_node_command, + ) + + async def _async_send_node_command(call: ServiceCall): + await hass.helpers.service.entity_service_call( + async_get_platforms(hass, DOMAIN), SERVICE_SEND_NODE_COMMAND, call + ) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SEND_NODE_COMMAND, + schema=cv.make_entity_service_schema(SERVICE_SEND_NODE_COMMAND_SCHEMA), + service_func=_async_send_node_command, + ) + @callback def async_unload_services(hass: HomeAssistantType): @@ -374,23 +399,8 @@ def async_unload_services(hass: HomeAssistantType): hass.services.async_remove(domain=DOMAIN, service=SERVICE_SET_VARIABLE) hass.services.async_remove(domain=DOMAIN, service=SERVICE_CLEANUP) hass.services.async_remove(domain=DOMAIN, service=SERVICE_RELOAD) - - -@callback -def async_setup_device_services(hass: HomeAssistantType): - """Create device-specific services for the ISY Integration.""" - platform = entity_platform.current_platform.get() - - platform.async_register_entity_service( - SERVICE_SEND_RAW_NODE_COMMAND, - SERVICE_SEND_RAW_NODE_COMMAND_SCHEMA, - SERVICE_SEND_RAW_NODE_COMMAND, - ) - platform.async_register_entity_service( - SERVICE_SEND_NODE_COMMAND, - SERVICE_SEND_NODE_COMMAND_SCHEMA, - SERVICE_SEND_NODE_COMMAND, - ) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_RAW_NODE_COMMAND) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_NODE_COMMAND) @callback diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 68a3bdeecd2..28d2264f283 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -10,7 +10,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids -from .services import async_setup_device_services async def async_setup_entry( @@ -29,7 +28,6 @@ async def async_setup_entry( await migrate_old_unique_ids(hass, SWITCH, devices) async_add_entities(devices) - async_setup_device_services(hass) class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 32ef88d361f..da1a3635d72 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -595,3 +595,19 @@ class EntityPlatform: current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar( "current_platform", default=None ) + + +@callback +def async_get_platforms( + hass: HomeAssistantType, integration_name: str +) -> List[EntityPlatform]: + """Find existing platforms.""" + if ( + DATA_ENTITY_PLATFORM not in hass.data + or integration_name not in hass.data[DATA_ENTITY_PLATFORM] + ): + return [] + + platforms: List[EntityPlatform] = hass.data[DATA_ENTITY_PLATFORM][integration_name] + + return platforms diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 89fcf45c29a..1c11afdb46b 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -9,7 +9,7 @@ from homeassistant.const import SERVICE_RELOAD from homeassistant.core import Event, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform -from homeassistant.helpers.entity_platform import DATA_ENTITY_PLATFORM, EntityPlatform +from homeassistant.helpers.entity_platform import EntityPlatform, async_get_platforms from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component @@ -141,13 +141,7 @@ def async_get_platform( hass: HomeAssistantType, integration_name: str, integration_platform_name: str ) -> Optional[EntityPlatform]: """Find an existing platform.""" - if ( - DATA_ENTITY_PLATFORM not in hass.data - or integration_name not in hass.data[DATA_ENTITY_PLATFORM] - ): - return None - - for integration_platform in hass.data[DATA_ENTITY_PLATFORM][integration_name]: + for integration_platform in async_get_platforms(hass, integration_name): if integration_platform.domain == integration_platform_name: platform: EntityPlatform = integration_platform return platform From 9ca7efbe4cde30e04a8a69d7234a4514de9151d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 15:35:41 +0000 Subject: [PATCH 753/862] Bumped version to 0.115.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cb926177ba5..e18296d679f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From c2f16cf21d06a44cc7d8f7cd5c9631e6b936128a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Sep 2020 23:11:42 +0200 Subject: [PATCH 754/862] Fix MQTT light value template (#39820) --- .../components/mqtt/light/schema_basic.py | 4 +++ tests/components/mqtt/test_light.py | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 54af71b3e05..e19fcbf0e40 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -35,6 +35,7 @@ from homeassistant.const import ( CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_UNIQUE_ID, + CONF_VALUE_TEMPLATE, STATE_ON, ) from homeassistant.core import callback @@ -157,6 +158,9 @@ async def async_setup_entity_basic( hass, config, async_add_entities, config_entry, discovery_data=None ): """Set up a MQTT Light.""" + if CONF_STATE_VALUE_TEMPLATE not in config and CONF_VALUE_TEMPLATE in config: + config[CONF_STATE_VALUE_TEMPLATE] = config[CONF_VALUE_TEMPLATE] + async_add_entities([MqttLight(hass, config, config_entry, discovery_data)]) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 56a5b4012c8..da4d90ad5ec 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -648,6 +648,35 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert state.attributes.get("xy_color") == (0.14, 0.131) +async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): + """Test the setting of the state with undocumented value_template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "value_template": "{{ value_json.hello }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): """Test the sending of command in optimistic mode.""" config = { From 807bfb71dff88ba08ac2729d56c4155072f04864 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Sep 2020 23:17:30 +0200 Subject: [PATCH 755/862] Update frontend to 20200908.0 (#39824) --- 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 65d0c6d3a21..379e08d30a7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200907.0"], + "requirements": ["home-assistant-frontend==20200908.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5382e95b5b8..99654fafb68 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.36.1 -home-assistant-frontend==20200907.0 +home-assistant-frontend==20200908.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 1a3afbfbba0..22688907370 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200907.0 +home-assistant-frontend==20200908.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9a40e0284c..b3b94a5c1da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200907.0 +home-assistant-frontend==20200908.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From d32e3dc31a6d9b552037dd916094c17c43acef76 Mon Sep 17 00:00:00 2001 From: Franchie <3265760+Franchie@users.noreply.github.com> Date: Tue, 8 Sep 2020 22:00:38 +0100 Subject: [PATCH 756/862] Avoid failing when hub does not provide cover position information (#39826) The powerview hub, seemingly randomly, will occasionally not provide data for cover positions. Some requests will return the desired response, but minutes later the same request might not. It appears this issue is being experienced by a number of people: https://community.home-assistant.io/t/hunter-douglas-powerview-component-expanding-this-api/88635/48 While an unfortunate bug with the hub, crashing the integration as a result of this missing data appears somewhat excessive. This patch adds a simple check to ensure the 'position' key has been returned by the hub before attempting to access its data. --- homeassistant/components/hunterdouglas_powerview/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 402451da26e..48b5f86cd02 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -215,7 +215,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): def _async_update_current_cover_position(self): """Update the current cover position from the data.""" _LOGGER.debug("Raw data update: %s", self._shade.raw_data) - position_data = self._shade.raw_data[ATTR_POSITION_DATA] + position_data = self._shade.raw_data.get(ATTR_POSITION_DATA, {}) if ATTR_POSITION1 in position_data: self._current_cover_position = position_data[ATTR_POSITION1] self._is_opening = False From 6cadc5b1574c4929a523ba8375764fc6bb5d243e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Sep 2020 21:18:08 +0000 Subject: [PATCH 757/862] Bumped version to 0.115.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e18296d679f..33be5ab0c0a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 5165d746aac748d70166f275c9e3ec5e0bc8498d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 8 Sep 2020 23:22:44 +0200 Subject: [PATCH 758/862] Add missing sensors after reworking sensor platform in Shelly integration (#39765) --- .../components/shelly/binary_sensor.py | 15 +++++++- homeassistant/components/shelly/sensor.py | 37 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index c1c241a32ef..c9a13249aa8 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_OPENING, + DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, DEVICE_CLASS_VIBRATION, BinarySensorEntity, @@ -15,8 +16,18 @@ from .entity import ( ) SENSORS = { - ("device", "overtemp"): BlockAttributeDescription(name="overtemp"), - ("relay", "overpower"): BlockAttributeDescription(name="overpower"), + ("device", "overtemp"): BlockAttributeDescription( + name="Overheating", device_class=DEVICE_CLASS_PROBLEM + ), + ("device", "overpower"): BlockAttributeDescription( + name="Over Power", device_class=DEVICE_CLASS_PROBLEM + ), + ("light", "overpower"): BlockAttributeDescription( + name="Over Power", device_class=DEVICE_CLASS_PROBLEM + ), + ("relay", "overpower"): BlockAttributeDescription( + name="Over Power", device_class=DEVICE_CLASS_PROBLEM + ), ("sensor", "dwIsOpened"): BlockAttributeDescription( name="Door", device_class=DEVICE_CLASS_OPENING ), diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 8e648a1a269..8a24a6380ed 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -40,6 +40,43 @@ SENSORS = { device_class=sensor.DEVICE_CLASS_POWER, default_enabled=False, ), + ("device", "power"): BlockAttributeDescription( + name="Power", + unit=POWER_WATT, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_POWER, + ), + ("emeter", "power"): BlockAttributeDescription( + name="Power", + unit=POWER_WATT, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_POWER, + ), + ("relay", "power"): BlockAttributeDescription( + name="Power", + unit=POWER_WATT, + value=lambda value: round(value, 1), + device_class=sensor.DEVICE_CLASS_POWER, + ), + ("device", "energy"): BlockAttributeDescription( + name="Energy", + unit=ENERGY_KILO_WATT_HOUR, + value=lambda value: round(value / 60 / 1000, 2), + device_class=sensor.DEVICE_CLASS_ENERGY, + ), + ("emeter", "energy"): BlockAttributeDescription( + name="Energy", + unit=ENERGY_KILO_WATT_HOUR, + value=lambda value: round(value / 1000, 2), + device_class=sensor.DEVICE_CLASS_ENERGY, + ), + ("light", "energy"): BlockAttributeDescription( + name="Energy", + unit=ENERGY_KILO_WATT_HOUR, + value=lambda value: round(value / 60 / 1000, 2), + device_class=sensor.DEVICE_CLASS_ENERGY, + default_enabled=False, + ), ("relay", "energy"): BlockAttributeDescription( name="Energy", unit=ENERGY_KILO_WATT_HOUR, From c91c9f2b409158996c3fa2474b37bb060cb5eed1 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 8 Sep 2020 23:42:45 +0200 Subject: [PATCH 759/862] Fix Kodi media browser (#39829) Co-authored-by: Paulus Schoutsen --- homeassistant/components/kodi/browse_media.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 1b1576e82da..cae982078db 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -5,8 +5,8 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_EPISODE, MEDIA_CLASS_MOVIE, - MEDIA_CLASS_MUSIC, MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_SEASON, MEDIA_CLASS_TV_SHOW, @@ -32,16 +32,18 @@ EXPANDABLE_MEDIA_TYPES = [ MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_SEASON, + "library_music", ] CONTENT_TYPE_MEDIA_CLASS = { - "library_music": MEDIA_CLASS_MUSIC, + "library_music": MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE, MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, + MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, } @@ -187,16 +189,22 @@ def item_payload(item, media_library): media_content_id = "" title = item["label"] - can_play = media_content_type in PLAYABLE_MEDIA_TYPES and bool(media_content_id) + can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES thumbnail = item.get("thumbnail") if thumbnail: thumbnail = media_library.thumbnail_url(thumbnail) + if media_content_type == MEDIA_TYPE_MOVIE and not media_content_id: + media_class = MEDIA_CLASS_DIRECTORY + can_expand = True + else: + media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] + return BrowseMedia( title=title, - media_class=CONTENT_TYPE_MEDIA_CLASS[item["type"]], + media_class=media_class, media_content_type=media_content_type, media_content_id=media_content_id, can_play=can_play, From 0458b5e3a6bb12979fad6c81d940a8cfafe3ad9d Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 9 Sep 2020 07:56:40 -0500 Subject: [PATCH 760/862] Fix nzbget sensors (#39833) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 - homeassistant/components/nzbget/sensor.py | 4 +- tests/components/nzbget/__init__.py | 20 ++++----- tests/components/nzbget/test_sensor.py | 54 +++++++++++++++++++++++ 4 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 tests/components/nzbget/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 1a46607e567..a86b6312a77 100644 --- a/.coveragerc +++ b/.coveragerc @@ -589,7 +589,6 @@ omit = homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py - homeassistant/components/nzbget/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/* homeassistant/components/oem/climate.py diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index c9437195826..ddbc73ca10a 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -61,7 +61,7 @@ async def async_setup_entry( ) ) - async_add_entities(sensors, True) + async_add_entities(sensors) class NZBGetSensor(NZBGetEntity, Entity): @@ -108,7 +108,7 @@ class NZBGetSensor(NZBGetEntity, Entity): @property def state(self): """Return the state of the sensor.""" - value = self.coordinator.data.status.get(self._sensor_type) + value = self.coordinator.data["status"].get(self._sensor_type) if value is None: _LOGGER.warning("Unable to locate value for %s", self._sensor_type) diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py index 8da67e2a0a2..8a36b299d87 100644 --- a/tests/components/nzbget/__init__.py +++ b/tests/components/nzbget/__init__.py @@ -48,16 +48,16 @@ YAML_CONFIG = { MOCK_VERSION = "21.0" MOCK_STATUS = { - "ArticleCacheMB": "64", - "AverageDownloadRate": "512", - "DownloadPaused": "4", - "DownloadRate": "1000", - "DownloadedSizeMB": "256", - "FreeDiskSpaceMB": "1024", - "PostJobCount": "2", - "PostPaused": "4", - "RemainingSizeMB": "512", - "UpTimeSec": "600", + "ArticleCacheMB": 64, + "AverageDownloadRate": 1250000, + "DownloadPaused": 4, + "DownloadRate": 2500000, + "DownloadedSizeMB": 256, + "FreeDiskSpaceMB": 1024, + "PostJobCount": 2, + "PostPaused": 4, + "RemainingSizeMB": 512, + "UpTimeSec": 600, } MOCK_HISTORY = [ diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py new file mode 100644 index 00000000000..43803384740 --- /dev/null +++ b/tests/components/nzbget/test_sensor.py @@ -0,0 +1,54 @@ +"""Test the NZBGet sensors.""" +from datetime import timedelta + +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + DATA_MEGABYTES, + DATA_RATE_MEGABYTES_PER_SECOND, + DEVICE_CLASS_TIMESTAMP, +) +from homeassistant.util import dt as dt_util + +from . import init_integration + +from tests.async_mock import patch + + +async def test_sensors(hass) -> None: + """Test the creation and values of the sensors.""" + now = dt_util.utcnow().replace(microsecond=0) + with patch("homeassistant.util.dt.utcnow", return_value=now): + entry = await init_integration(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + uptime = now - timedelta(seconds=600) + + sensors = { + "article_cache": ("ArticleCacheMB", "64", DATA_MEGABYTES, None), + "average_speed": ( + "AverageDownloadRate", + "1.19", + DATA_RATE_MEGABYTES_PER_SECOND, + None, + ), + "download_paused": ("DownloadPaused", "4", None, None), + "speed": ("DownloadRate", "2.38", DATA_RATE_MEGABYTES_PER_SECOND, None), + "size": ("DownloadedSizeMB", "256", DATA_MEGABYTES, None), + "disk_free": ("FreeDiskSpaceMB", "1024", DATA_MEGABYTES, None), + "post_processing_jobs": ("PostJobCount", "2", "Jobs", None), + "post_processing_paused": ("PostPaused", "4", None, None), + "queue_size": ("RemainingSizeMB", "512", DATA_MEGABYTES, None), + "uptime": ("UpTimeSec", uptime.isoformat(), None, DEVICE_CLASS_TIMESTAMP), + } + + for (sensor_id, data) in sensors.items(): + entity_entry = registry.async_get(f"sensor.nzbgettest_{sensor_id}") + assert entity_entry + assert entity_entry.device_class == data[3] + assert entity_entry.unique_id == f"{entry.entry_id}_{data[0]}" + + state = hass.states.get(f"sensor.nzbgettest_{sensor_id}") + assert state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] + assert state.state == data[1] From 139a0ca00836bc94ca76ef20b2cb388b7d4d1d4e Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 9 Sep 2020 14:12:11 +0200 Subject: [PATCH 761/862] Fix Kodi media browser (#39840) * Refactor * Make linter happy * Only return at the end * Handle exception --- homeassistant/components/kodi/browse_media.py | 112 +++++++++++------- 1 file changed, 67 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index cae982078db..3fc3b40cd38 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -1,14 +1,17 @@ """Support for media browsing.""" +import logging -from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player import BrowseError, BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_EPISODE, MEDIA_CLASS_MOVIE, + MEDIA_CLASS_MUSIC, MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_SEASON, + MEDIA_CLASS_TRACK, MEDIA_CLASS_TV_SHOW, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -26,26 +29,24 @@ PLAYABLE_MEDIA_TYPES = [ MEDIA_TYPE_TRACK, ] -EXPANDABLE_MEDIA_TYPES = [ - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_SEASON, - "library_music", -] - CONTENT_TYPE_MEDIA_CLASS = { - "library_music": MEDIA_CLASS_DIRECTORY, + "library_music": MEDIA_CLASS_MUSIC, MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE, MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK, MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, } +_LOGGER = logging.getLogger(__name__) + + +class UnknownMediaType(BrowseError): + """Unknown media type.""" + async def build_item_response(media_library, payload): """Create response payload for the provided media query.""" @@ -141,16 +142,23 @@ async def build_item_response(media_library, payload): title = season["seasondetails"]["label"] if media is None: - return + return None + + children = [] + for item in media: + try: + children.append(item_payload(item, media_library)) + except UnknownMediaType: + pass return BrowseMedia( media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], - media_content_id=payload["search_id"], + media_content_id=search_id, media_content_type=search_type, title=title, can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id, can_expand=True, - children=[item_payload(item, media_library) for item in media], + children=children, thumbnail=thumbnail, ) @@ -161,46 +169,60 @@ def item_payload(item, media_library): Used by async_browse_media. """ - if "songid" in item: - media_content_type = MEDIA_TYPE_TRACK - media_content_id = f"{item['songid']}" - elif "albumid" in item: - media_content_type = MEDIA_TYPE_ALBUM - media_content_id = f"{item['albumid']}" - elif "artistid" in item: - media_content_type = MEDIA_TYPE_ARTIST - media_content_id = f"{item['artistid']}" - elif "movieid" in item: - media_content_type = MEDIA_TYPE_MOVIE - media_content_id = f"{item['movieid']}" - elif "episodeid" in item: - media_content_type = MEDIA_TYPE_EPISODE - media_content_id = f"{item['episodeid']}" - elif "seasonid" in item: - media_content_type = MEDIA_TYPE_SEASON - media_content_id = f"{item['tvshowid']}/{item['season']}" - elif "tvshowid" in item: - media_content_type = MEDIA_TYPE_TVSHOW - media_content_id = f"{item['tvshowid']}" - else: - # this case is for the top folder of each type - # possible content types: album, artist, movie, library_music, tvshow - media_content_type = item.get("type") - media_content_id = "" - title = item["label"] - can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id - can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES thumbnail = item.get("thumbnail") if thumbnail: thumbnail = media_library.thumbnail_url(thumbnail) - if media_content_type == MEDIA_TYPE_MOVIE and not media_content_id: - media_class = MEDIA_CLASS_DIRECTORY + if "songid" in item: + media_content_type = MEDIA_TYPE_TRACK + media_content_id = f"{item['songid']}" + can_play = True + can_expand = False + elif "albumid" in item: + media_content_type = MEDIA_TYPE_ALBUM + media_content_id = f"{item['albumid']}" + can_play = True + can_expand = True + elif "artistid" in item: + media_content_type = MEDIA_TYPE_ARTIST + media_content_id = f"{item['artistid']}" + can_play = True + can_expand = True + elif "movieid" in item: + media_content_type = MEDIA_TYPE_MOVIE + media_content_id = f"{item['movieid']}" + can_play = True + can_expand = False + elif "episodeid" in item: + media_content_type = MEDIA_TYPE_EPISODE + media_content_id = f"{item['episodeid']}" + can_play = True + can_expand = False + elif "seasonid" in item: + media_content_type = MEDIA_TYPE_SEASON + media_content_id = f"{item['tvshowid']}/{item['season']}" + can_play = False + can_expand = True + elif "tvshowid" in item: + media_content_type = MEDIA_TYPE_TVSHOW + media_content_id = f"{item['tvshowid']}" + can_play = False can_expand = True else: + # this case is for the top folder of each type + # possible content types: album, artist, movie, library_music, tvshow + media_content_type = item["type"] + media_content_id = "" + can_play = False + can_expand = True + + try: media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received: %s", media_content_type) + raise UnknownMediaType from err return BrowseMedia( title=title, From b572c0df7fda1eb8b0de574229b15c6e80b85df1 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 9 Sep 2020 14:48:28 +0200 Subject: [PATCH 762/862] Make spotify media class lookup more robust (#39841) --- .../components/spotify/media_player.py | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 91e0c85aa3c..eb05fcc4ddc 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -14,6 +14,7 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_EPISODE, MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_PODCAST, MEDIA_CLASS_TRACK, @@ -118,7 +119,9 @@ CONTENT_TYPE_MEDIA_CLASS = { MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST, + MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK, } @@ -126,6 +129,10 @@ class MissingMediaInformation(BrowseError): """Missing media required information.""" +class UnknownMediaType(BrowseError): + """Unknown media type.""" + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -526,10 +533,16 @@ def build_item_response(spotify, user, payload): if media is None: return None + try: + media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] + except KeyError: + _LOGGER.debug("Unknown media type received: %s", media_content_type) + return None + if media_content_type == "categories": media_item = BrowseMedia( title=LIBRARY_MAP.get(media_content_id), - media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type], + media_class=media_class, media_content_id=media_content_id, media_content_type=media_content_type, can_play=False, @@ -562,7 +575,7 @@ def build_item_response(spotify, user, payload): response = { "title": title, - "media_class": CONTENT_TYPE_MEDIA_CLASS[media_content_type], + "media_class": media_class, "media_content_id": media_content_id, "media_content_type": media_content_type, "can_play": media_content_type in PLAYABLE_MEDIA_TYPES, @@ -572,7 +585,7 @@ def build_item_response(spotify, user, payload): for item in items: try: response["children"].append(item_payload(item)) - except MissingMediaInformation: + except (MissingMediaInformation, UnknownMediaType): continue if "images" in media: @@ -596,6 +609,12 @@ def item_payload(item): _LOGGER.debug("Missing type or uri for media item: %s", item) raise MissingMediaInformation from err + try: + media_class = CONTENT_TYPE_MEDIA_CLASS[media_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received: %s", media_type) + raise UnknownMediaType from err + can_expand = media_type not in [ MEDIA_TYPE_TRACK, MEDIA_TYPE_EPISODE, @@ -611,7 +630,7 @@ def item_payload(item): payload = { **payload, - "media_class": CONTENT_TYPE_MEDIA_CLASS[media_type], + "media_class": media_class, } if "images" in item: From 1333e23c23a9a9558ebb1a4fab92d12486a70057 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Sep 2020 13:15:53 +0000 Subject: [PATCH 763/862] Bumped version to 0.115.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 33be5ab0c0a..c038d06bce2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 4af6804c50c35e13872e4db07a5cf96e98e0b755 Mon Sep 17 00:00:00 2001 From: Colin Frei Date: Wed, 9 Sep 2020 20:19:37 +0200 Subject: [PATCH 764/862] Use correct URL for Fitbit callbacks (#39823) --- homeassistant/components/fitbit/sensor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 56afae1e9a7..f0914ab35f0 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -185,7 +185,9 @@ def request_app_setup(hass, config, add_entities, config_path, discovery_info=No else: setup_platform(hass, config, add_entities, discovery_info) - start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" + start_url = ( + f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" + ) description = f"""Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. @@ -220,7 +222,7 @@ def request_oauth_completion(hass): def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" - start_url = f"{get_url(hass)}{FITBIT_AUTH_START}" + start_url = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_START}" description = f"Please authorize Fitbit by visiting {start_url}" @@ -312,7 +314,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET) ) - redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" + redirect_uri = ( + f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" + ) fitbit_auth_start_url, _ = oauth.authorize_token_url( redirect_uri=redirect_uri, @@ -357,7 +361,7 @@ class FitbitAuthCallbackView(HomeAssistantView): result = None if data.get("code") is not None: - redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" + redirect_uri = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}" try: result = self.oauth.fetch_access_token(data.get("code"), redirect_uri) From 8e3e2d436e40910e8467ce9b00daf59d7eca4eaf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Sep 2020 15:19:14 -0500 Subject: [PATCH 765/862] Use a unique id for each icmplib ping to avoid mixing unrelated responses (#39830) --- homeassistant/components/ping/__init__.py | 24 +++++++++++++++++++ .../components/ping/binary_sensor.py | 23 ++++++++++++------ .../components/ping/device_tracker.py | 13 ++++++++-- homeassistant/components/ping/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 54 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index d5ec35276cf..19207ada1b7 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -1,4 +1,28 @@ """The ping component.""" +from homeassistant.core import callback + DOMAIN = "ping" PLATFORMS = ["binary_sensor"] + +PING_ID = "ping_id" +DEFAULT_START_ID = 129 +MAX_PING_ID = 65534 + + +@callback +def async_get_next_ping_id(hass): + """Find the next id to use in the outbound ping. + + Must be called in async + """ + current_id = hass.data.setdefault(DOMAIN, {}).get(PING_ID, DEFAULT_START_ID) + + if current_id == MAX_PING_ID: + next_id = DEFAULT_START_ID + else: + next_id = current_id + 1 + + hass.data[DOMAIN][PING_ID] = next_id + + return next_id diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index cb0d025f65d..5befe0b7f3a 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -1,6 +1,7 @@ """Tracks the latency of a host by sending ICMP echo requests (ping).""" import asyncio from datetime import timedelta +from functools import partial import logging import re import sys @@ -14,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import setup_reload_service -from . import DOMAIN, PLATFORMS +from . import DOMAIN, PLATFORMS, async_get_next_ping_id from .const import PING_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -131,20 +132,28 @@ class PingData: class PingDataICMPLib(PingData): """The Class for handling the data retrieval using icmplib.""" - def ping(self): - """Send ICMP echo request and return details.""" - return icmp_ping(self._ip_address, count=self._count) - async def async_update(self) -> None: """Retrieve the latest details from the host.""" - data = await self.hass.async_add_executor_job(self.ping) + _LOGGER.warning("ping address: %s", self._ip_address) + data = await self.hass.async_add_executor_job( + partial( + icmp_ping, + self._ip_address, + count=self._count, + id=async_get_next_ping_id(self.hass), + ) + ) + self.available = data.is_alive + if not self.available: + self.data = False + return + self.data = { "min": data.min_rtt, "max": data.max_rtt, "avg": data.avg_rtt, "mdev": "", } - self.available = data.is_alive class PingDataSubProcess(PingData): diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index cbbce13171b..ea2d7a526e3 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -15,8 +15,10 @@ from homeassistant.components.device_tracker.const import ( SOURCE_TYPE_ROUTER, ) import homeassistant.helpers.config_validation as cv +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.process import kill_subprocess +from . import async_get_next_ping_id from .const import PING_ATTEMPTS_COUNT, PING_TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -76,15 +78,22 @@ class HostSubProcess: class HostICMPLib: """Host object with ping detection.""" - def __init__(self, ip_address, dev_id, _, config): + def __init__(self, ip_address, dev_id, hass, config): """Initialize the Host pinger.""" + self.hass = hass self.ip_address = ip_address self.dev_id = dev_id self._count = config[CONF_PING_COUNT] def ping(self): """Send an ICMP echo request and return True if success.""" - return icmp_ping(self.ip_address, count=PING_ATTEMPTS_COUNT).is_alive + next_id = run_callback_threadsafe( + self.hass.loop, async_get_next_ping_id, self.hass + ).result() + + return icmp_ping( + self.ip_address, count=PING_ATTEMPTS_COUNT, id=next_id + ).is_alive def update(self, see): """Update device state by sending one or more ping messages.""" diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index f23697808a2..675ee1a9586 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -3,6 +3,6 @@ "name": "Ping (ICMP)", "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], - "requirements": ["icmplib==1.1.1"], + "requirements": ["icmplib==1.1.3"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 22688907370..5d2a8e708df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -789,7 +789,7 @@ ibm-watson==4.0.1 ibmiotf==0.3.4 # homeassistant.components.ping -icmplib==1.1.1 +icmplib==1.1.3 # homeassistant.components.iglo iglo==1.2.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3b94a5c1da..876f391688c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -392,7 +392,7 @@ huawei-lte-api==1.4.12 iaqualink==0.3.4 # homeassistant.components.ping -icmplib==1.1.1 +icmplib==1.1.3 # homeassistant.components.influxdb influxdb-client==1.8.0 From 5ae0844f35b51a6270c4d20739472fd32c80d3ff Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 9 Sep 2020 22:30:40 +0200 Subject: [PATCH 766/862] Updated warning_device_warn (#39851) duty_cycle: spec says in inrements of 10 duration: its a 16 bit field --- homeassistant/components/zha/services.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 971321fbfd2..257d1026f7f 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -132,11 +132,11 @@ warning_device_warn: example: "00:0d:6f:00:05:7d:2d:34" mode: description: >- - The Warning Mode field is used as an 4-bit enumeration, can have one of the values defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. + The Warning Mode field is used as an 4-bit enumeration, can have one of the values 0-6 defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. example: 1 strobe: description: >- - The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. + The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. "0" means no strobe, "1" means strobe. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. example: 1 level: description: >- @@ -144,12 +144,12 @@ warning_device_warn: example: 2 duration: description: >- - Requested duration of warning, in seconds. If both Strobe and Warning Mode are "0" this field SHALL be ignored. + Requested duration of warning, in seconds (16 bit). If both Strobe and Warning Mode are "0" this field SHALL be ignored. example: 2 duty_cycle: description: >- Indicates the length of the flash cycle. This provides a means of varying the flash duration for different alarm types (e.g., fire, police, burglar). Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for 6/10ths of a second. - example: 2 + example: 50 intensity: description: >- Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec. From 578c1b283a6673a328ba9b4316a6fde667af92e0 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Wed, 9 Sep 2020 16:19:30 -0400 Subject: [PATCH 767/862] Sort Local Media Source and fix media class (#39858) --- homeassistant/components/media_source/const.py | 11 +++++++++++ .../components/media_source/local_source.py | 13 ++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py index d50a8b1c404..68a8244c3ce 100644 --- a/homeassistant/components/media_source/const.py +++ b/homeassistant/components/media_source/const.py @@ -1,7 +1,18 @@ """Constants for the media_source integration.""" import re +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_IMAGE, + MEDIA_CLASS_MUSIC, + MEDIA_CLASS_VIDEO, +) + DOMAIN = "media_source" MEDIA_MIME_TYPES = ("audio", "video", "image") +MEDIA_CLASS_MAP = { + "audio": MEDIA_CLASS_MUSIC, + "video": MEDIA_CLASS_VIDEO, + "image": MEDIA_CLASS_IMAGE, +} URI_SCHEME = "media-source://" URI_SCHEME_REGEX = re.compile(r"^media-source://(?P[^/]+)?(?P.+)?") diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 774ee64d852..a558de775f8 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -12,7 +12,7 @@ from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant, callback from homeassistant.util import sanitize_path -from .const import DOMAIN, MEDIA_MIME_TYPES +from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia @@ -112,11 +112,15 @@ class LocalSource(MediaSource): if is_dir: title += "/" + media_class = MEDIA_CLASS_MAP.get( + mime_type and mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY + ) + media = BrowseMediaSource( domain=DOMAIN, identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type="directory", + media_class=media_class, + media_content_type=mime_type or "", title=title, can_play=is_file, can_expand=is_dir, @@ -132,6 +136,9 @@ class LocalSource(MediaSource): if child: media.children.append(child) + # Sort children showing directories first, then by name + media.children.sort(key=lambda child: (child.can_play, child.title)) + return media From f79ce7bd042e963ed637aa4132bc725742b987c0 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 9 Sep 2020 15:08:55 -0400 Subject: [PATCH 768/862] Update ZHA dependency (#39862) --- 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 1fd8bb71920..4bde073a933 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zha-quirks==0.0.44", "zigpy-cc==0.5.2", "zigpy-deconz==0.9.2", - "zigpy==0.23.1", + "zigpy==0.23.2", "zigpy-xbee==0.13.0", "zigpy-zigate==0.6.2", "zigpy-znp==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 5d2a8e708df..2490e6b366d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2314,7 +2314,7 @@ zigpy-zigate==0.6.2 zigpy-znp==0.1.1 # homeassistant.components.zha -zigpy==0.23.1 +zigpy==0.23.2 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 876f391688c..618facba40a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1074,4 +1074,4 @@ zigpy-zigate==0.6.2 zigpy-znp==0.1.1 # homeassistant.components.zha -zigpy==0.23.1 +zigpy==0.23.2 From c5cf95c14bd9ba69ade22c9562943c16ebc36e8f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 9 Sep 2020 22:19:52 +0200 Subject: [PATCH 769/862] Remove media class apps and channels (#39864) --- homeassistant/components/media_player/const.py | 2 -- homeassistant/components/media_source/models.py | 3 +-- homeassistant/components/philips_js/media_player.py | 3 +-- homeassistant/components/roku/media_player.py | 10 ++++------ 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 6714d03c19e..0035fc9f4d2 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -31,10 +31,8 @@ DOMAIN = "media_player" MEDIA_CLASS_ALBUM = "album" MEDIA_CLASS_APP = "app" -MEDIA_CLASS_APPS = "apps" MEDIA_CLASS_ARTIST = "artist" MEDIA_CLASS_CHANNEL = "channel" -MEDIA_CLASS_CHANNELS = "channels" MEDIA_CLASS_COMPOSER = "composer" MEDIA_CLASS_CONTRIBUTING_ARTIST = "contributing_artist" MEDIA_CLASS_DIRECTORY = "directory" diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 5d768fd79d8..3b2044d7e0f 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -6,7 +6,6 @@ from typing import List, Optional, Tuple from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, ) @@ -54,7 +53,7 @@ class MediaSourceItem: base = BrowseMediaSource( domain=None, identifier=None, - media_class=MEDIA_CLASS_CHANNELS, + media_class=MEDIA_CLASS_CHANNEL, media_content_type=MEDIA_TYPE_CHANNELS, title="Media Sources", can_play=False, diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index a780fe6b635..7da485fca74 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -12,7 +12,6 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_CHANNELS, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, @@ -290,7 +289,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): return BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_CHANNELS, + media_class=MEDIA_CLASS_CHANNEL, media_content_id="", media_content_type=MEDIA_TYPE_CHANNELS, can_play=False, diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 7935206f114..3438b8c7add 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -12,9 +12,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( MEDIA_CLASS_APP, - MEDIA_CLASS_APPS, MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_CHANNELS, MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_APP, MEDIA_TYPE_APPS, @@ -95,7 +93,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Apps", - media_class=MEDIA_CLASS_APPS, + media_class=MEDIA_CLASS_APP, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -107,7 +105,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia: library_info.children.append( BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_CHANNELS, + media_class=MEDIA_CLASS_CHANNEL, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, @@ -294,7 +292,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_APPS: response = BrowseMedia( title="Apps", - media_class=MEDIA_CLASS_APPS, + media_class=MEDIA_CLASS_APP, media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, can_expand=True, @@ -316,7 +314,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if media_content_type == MEDIA_TYPE_CHANNELS: response = BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_CHANNELS, + media_class=MEDIA_CLASS_CHANNEL, media_content_id="channels", media_content_type=MEDIA_TYPE_CHANNELS, can_expand=True, From 6d7dfc0804e151ce7087748f514f89644b21cdd2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Sep 2020 20:35:18 +0000 Subject: [PATCH 770/862] Bumped version to 0.115.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c038d06bce2..a20d0effdaf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 4578baca3ec0b23799ff1c7320b8fc7522a76c40 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 9 Sep 2020 16:22:26 -0500 Subject: [PATCH 771/862] Improve Roku media browser structure (#39754) Co-authored-by: Martin Hjelmare --- homeassistant/components/roku/browse_media.py | 142 ++++++++++++++++++ homeassistant/components/roku/media_player.py | 97 +----------- 2 files changed, 149 insertions(+), 90 deletions(-) create mode 100644 homeassistant/components/roku/browse_media.py diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py new file mode 100644 index 00000000000..809c6ac3578 --- /dev/null +++ b/homeassistant/components/roku/browse_media.py @@ -0,0 +1,142 @@ +"""Support for media browsing.""" + +from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_APP, + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, + MEDIA_TYPE_APP, + MEDIA_TYPE_APPS, + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_CHANNELS, +) + +CONTENT_TYPE_MEDIA_CLASS = { + MEDIA_TYPE_APP: MEDIA_CLASS_APP, + MEDIA_TYPE_APPS: MEDIA_CLASS_APP, + MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, + MEDIA_TYPE_CHANNELS: MEDIA_CLASS_CHANNEL, +} + +PLAYABLE_MEDIA_TYPES = [ + MEDIA_TYPE_APP, + MEDIA_TYPE_CHANNEL, +] + +EXPANDABLE_MEDIA_TYPES = [ + MEDIA_TYPE_APPS, + MEDIA_TYPE_CHANNELS, +] + + +def build_item_response(coordinator, payload): + """Create response payload for the provided media query.""" + search_id = payload["search_id"] + search_type = payload["search_type"] + + thumbnail = None + title = None + media = None + + if search_type == MEDIA_TYPE_APPS: + title = "Apps" + media = [ + {"app_id": item.app_id, "title": item.name, "type": MEDIA_TYPE_APP} + for item in coordinator.data.apps + ] + elif search_type == MEDIA_TYPE_CHANNELS: + title = "Channels" + media = [ + { + "channel_number": item.number, + "title": item.name, + "type": MEDIA_TYPE_CHANNEL, + } + for item in coordinator.data.channels + ] + + if media is None: + return None + + return BrowseMedia( + media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], + media_content_id=search_id, + media_content_type=search_type, + title=title, + can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id, + can_expand=True, + children=[item_payload(item, coordinator) for item in media], + thumbnail=thumbnail, + ) + + +def item_payload(item, coordinator): + """ + Create response payload for a single media item. + + Used by async_browse_media. + """ + thumbnail = None + + if "app_id" in item: + media_content_type = MEDIA_TYPE_APP + media_content_id = item["app_id"] + thumbnail = coordinator.roku.app_icon_url(item["app_id"]) + elif "channel_number" in item: + media_content_type = MEDIA_TYPE_CHANNEL + media_content_id = item["channel_number"] + else: + media_content_type = item["type"] + media_content_id = "" + + title = item["title"] + can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id + can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES + + return BrowseMedia( + title=title, + media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type], + media_content_type=media_content_type, + media_content_id=media_content_id, + can_play=can_play, + can_expand=can_expand, + thumbnail=thumbnail, + ) + + +def library_payload(coordinator): + """ + Create response payload to describe contents of a specific library. + + Used by async_browse_media. + """ + library_info = BrowseMedia( + media_class=MEDIA_CLASS_DIRECTORY, + media_content_id="library", + media_content_type="library", + title="Media Library", + can_play=False, + can_expand=True, + children=[], + ) + + library = { + MEDIA_TYPE_APPS: "Apps", + MEDIA_TYPE_CHANNELS: "Channels", + } + + for item in [{"title": name, "type": type_} for type_, name in library.items()]: + if ( + item["type"] == MEDIA_TYPE_CHANNELS + and coordinator.data.info.device_type != "tv" + ): + continue + + library_info.children.append( + item_payload( + {"title": item["title"], "type": item["type"]}, + coordinator, + ) + ) + + return library_info diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 3438b8c7add..0e035106824 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -7,17 +7,11 @@ import voluptuous as vol from homeassistant.components.media_player import ( DEVICE_CLASS_RECEIVER, DEVICE_CLASS_TV, - BrowseMedia, MediaPlayerEntity, ) from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -42,6 +36,7 @@ from homeassistant.const import ( from homeassistant.helpers import entity_platform from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler +from .browse_media import build_item_response, library_payload from .const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH _LOGGER = logging.getLogger(__name__) @@ -78,44 +73,6 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -def browse_media_library(channels: bool = False) -> BrowseMedia: - """Create response payload to describe contents of a specific library.""" - library_info = BrowseMedia( - title="Media Library", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_id="library", - media_content_type="library", - can_play=False, - can_expand=True, - children=[], - ) - - library_info.children.append( - BrowseMedia( - title="Apps", - media_class=MEDIA_CLASS_APP, - media_content_id="apps", - media_content_type=MEDIA_TYPE_APPS, - can_expand=True, - can_play=False, - ) - ) - - if channels: - library_info.children.append( - BrowseMedia( - title="Channels", - media_class=MEDIA_CLASS_CHANNEL, - media_content_id="channels", - media_content_type=MEDIA_TYPE_CHANNELS, - can_expand=True, - can_play=False, - ) - ) - - return library_info - - class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """Representation of a Roku media player on the network.""" @@ -284,53 +241,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" if media_content_type in [None, "library"]: - is_tv = self.coordinator.data.info.device_type == "tv" - return browse_media_library(channels=is_tv) + return library_payload(self.coordinator) - response = None - - if media_content_type == MEDIA_TYPE_APPS: - response = BrowseMedia( - title="Apps", - media_class=MEDIA_CLASS_APP, - media_content_id="apps", - media_content_type=MEDIA_TYPE_APPS, - can_expand=True, - can_play=False, - children=[ - BrowseMedia( - title=app.name, - thumbnail=self.coordinator.roku.app_icon_url(app.app_id), - media_class=MEDIA_CLASS_APP, - media_content_id=app.app_id, - media_content_type=MEDIA_TYPE_APP, - can_play=True, - can_expand=False, - ) - for app in self.coordinator.data.apps - ], - ) - - if media_content_type == MEDIA_TYPE_CHANNELS: - response = BrowseMedia( - title="Channels", - media_class=MEDIA_CLASS_CHANNEL, - media_content_id="channels", - media_content_type=MEDIA_TYPE_CHANNELS, - can_expand=True, - can_play=False, - children=[ - BrowseMedia( - title=channel.name, - media_class=MEDIA_CLASS_CHANNEL, - media_content_id=channel.number, - media_content_type=MEDIA_TYPE_CHANNEL, - can_play=True, - can_expand=False, - ) - for channel in self.coordinator.data.channels - ], - ) + payload = { + "search_type": media_content_type, + "search_id": media_content_id, + } + response = build_item_response(self.coordinator, payload) if response is None: raise BrowseError( From be28dc0bca84e914c2ec3a67b84d96177f532783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 10 Sep 2020 14:52:49 +0200 Subject: [PATCH 772/862] Add exception for NoURLAvailableError in OAuth2FlowHandler (#39845) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- homeassistant/components/almond/strings.json | 3 +- .../components/home_connect/strings.json | 3 +- homeassistant/components/netatmo/strings.json | 5 +- homeassistant/components/smappee/strings.json | 61 ++++++++++--------- homeassistant/components/somfy/strings.json | 3 +- homeassistant/components/spotify/strings.json | 1 + homeassistant/components/toon/strings.json | 3 +- .../components/withings/strings.json | 3 +- .../helpers/config_entry_oauth2_flow.py | 9 ++- homeassistant/strings.json | 3 +- .../helpers/test_config_entry_oauth2_flow.py | 17 ++++++ 11 files changed, 72 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json index 008d21c463b..e8244798e81 100644 --- a/homeassistant/components/almond/strings.json +++ b/homeassistant/components/almond/strings.json @@ -10,7 +10,8 @@ "abort": { "already_setup": "You can only configure one Almond account.", "cannot_connect": "Unable to connect to the Almond server.", - "missing_configuration": "Please check the documentation on how to set up Almond." + "missing_configuration": "Please check the documentation on how to set up Almond.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" } } } diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json index 6125897c962..798fe2930a0 100644 --- a/homeassistant/components/home_connect/strings.json +++ b/homeassistant/components/home_connect/strings.json @@ -6,7 +6,8 @@ } }, "abort": { - "missing_configuration": "The Home Connect component is not configured. Please follow the documentation." + "missing_configuration": "The Home Connect component is not configured. Please follow the documentation.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" }, "create_entry": { "default": "Successfully authenticated with Home Connect." diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index f1b761dd187..6e88d191610 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -8,7 +8,8 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]" + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" @@ -39,4 +40,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/smappee/strings.json b/homeassistant/components/smappee/strings.json index 9d4bb618832..1bec8fda0cc 100644 --- a/homeassistant/components/smappee/strings.json +++ b/homeassistant/components/smappee/strings.json @@ -1,34 +1,35 @@ { - "config": { - "flow_title": "Smappee: {name}", - "step": { - "environment": { - "description": "Set up your Smappee to integrate with Home Assistant.", - "data": { - "environment": "Environment" - } - }, - "local": { - "description": "Enter the host to initiate the Smappee local integration", - "data": { - "host": "[%key:common::config_flow::data::host%]" - } - }, - "zeroconf_confirm": { - "description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?", - "title": "Discovered Smappee device" - }, - "pick_implementation": { - "title": "Pick Authentication Method" - } - }, - "abort": { - "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", - "already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.", - "authorize_url_timeout": "Timeout generating authorize url.", - "connection_error": "Failed to connect to Smappee device.", - "missing_configuration": "The component is not configured. Please follow the documentation.", - "invalid_mdns": "Unsupported device for the Smappee integration." + "config": { + "flow_title": "Smappee: {name}", + "step": { + "environment": { + "description": "Set up your Smappee to integrate with Home Assistant.", + "data": { + "environment": "Environment" } + }, + "local": { + "description": "Enter the host to initiate the Smappee local integration", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?", + "title": "Discovered Smappee device" + }, + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.", + "authorize_url_timeout": "Timeout generating authorize url.", + "connection_error": "Failed to connect to Smappee device.", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "invalid_mdns": "Unsupported device for the Smappee integration.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" } + } } diff --git a/homeassistant/components/somfy/strings.json b/homeassistant/components/somfy/strings.json index 90ea98f7a87..d1fa921bb8e 100644 --- a/homeassistant/components/somfy/strings.json +++ b/homeassistant/components/somfy/strings.json @@ -6,7 +6,8 @@ "abort": { "already_setup": "You can only configure one Somfy account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Somfy component is not configured. Please follow the documentation." + "missing_configuration": "The Somfy component is not configured. Please follow the documentation.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" }, "create_entry": { "default": "Successfully authenticated with Somfy." } } diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index 85ff9ff267b..8e3fa6fc679 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -10,6 +10,7 @@ "abort": { "already_setup": "You can only configure one Spotify account.", "authorize_url_timeout": "Timeout generating authorize url.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", "missing_configuration": "The Spotify integration is not configured. Please follow the documentation.", "reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication." }, diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json index 05eef817d28..c5ac07516b6 100644 --- a/homeassistant/components/toon/strings.json +++ b/homeassistant/components/toon/strings.json @@ -17,7 +17,8 @@ "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "no_agreements": "This account has no Toon displays." + "no_agreements": "This account has no Toon displays.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" } } } diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index e7763a1db0c..c9d2d7ca22c 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -19,7 +19,8 @@ "abort": { "authorize_url_timeout": "Timeout generating authorize url.", "missing_configuration": "The Withings integration is not configured. Please follow the documentation.", - "already_configured": "Configuration updated for profile." + "already_configured": "Configuration updated for profile.", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" }, "create_entry": { "default": "Successfully authenticated with Withings." } } diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index da86c222c13..55ec3984b82 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -21,7 +21,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from .aiohttp_client import async_get_clientsession @@ -251,6 +251,13 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): url = await self.flow_impl.async_generate_authorize_url(self.flow_id) except asyncio.TimeoutError: return self.async_abort(reason="authorize_url_timeout") + except NoURLAvailableError: + return self.async_abort( + reason="no_url_available", + description_placeholders={ + "docs_url": "https://www.home-assistant.io/more-info/no-url-available" + }, + ) url = str(URL(url).update_query(self.extra_authorize_data)) diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 63615094715..05bc2e3c247 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -53,7 +53,8 @@ "already_configured_device": "Device is already configured", "no_devices_found": "No devices found on the network", "oauth2_missing_configuration": "The component is not configured. Please follow the documentation.", - "oauth2_authorize_url_timeout": "Timeout generating authorize URL." + "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", + "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" } } } diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index dc34b0f7876..691b2e93d56 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -8,6 +8,7 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.config import async_process_ha_core_config from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.network import NoURLAvailableError from tests.async_mock import patch from tests.common import MockConfigEntry, mock_platform @@ -128,6 +129,22 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): assert result["reason"] == "authorize_url_timeout" +async def test_abort_if_no_url_available(hass, flow_handler, local_impl): + """Check no_url_available generating authorization url.""" + flow_handler.async_register_implementation(hass, local_impl) + + flow = flow_handler() + flow.hass = hass + + with patch.object( + local_impl, "async_generate_authorize_url", side_effect=NoURLAvailableError + ): + result = await flow.async_step_user() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_url_available" + + async def test_abort_if_oauth_error( hass, flow_handler, local_impl, aiohttp_client, aioclient_mock, current_request ): From fe371f04385d2b8357891f9bd7d5560e545b30de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 00:36:58 +0200 Subject: [PATCH 773/862] Install stdlib-list in script/bootstrap (#39866) --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index 2b599950625..3166b8c7701 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -8,4 +8,4 @@ cd "$(dirname "$0")/.." echo "Installing development dependencies..." 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 +python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) --constraint homeassistant/package_constraints.txt From 668c73010a7aa7422382ad4d58831cbcfa665dc6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Sep 2020 07:58:40 +0200 Subject: [PATCH 774/862] Disable Met.no hourly weather by default (#39867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Hjelseth Høyer --- homeassistant/components/met/weather.py | 22 +++++++++++++----- tests/components/met/test_weather.py | 27 ++++++++++++++++++++++- tests/components/onboarding/test_views.py | 2 +- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index a53f66ab1dc..3355c497aab 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -104,7 +104,6 @@ class MetWeather(CoordinatorEntity, WeatherEntity): self._config = config self._is_metric = is_metric self._hourly = hourly - self._name_appendix = "-hourly" if hourly else "" @property def track_home(self): @@ -114,23 +113,34 @@ class MetWeather(CoordinatorEntity, WeatherEntity): @property def unique_id(self): """Return unique ID.""" + name_appendix = "" + if self._hourly: + name_appendix = "-hourly" if self.track_home: - return f"home{self._name_appendix}" + return f"home{name_appendix}" - return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{self._name_appendix}" + return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}" @property def name(self): """Return the name of the sensor.""" name = self._config.get(CONF_NAME) + name_appendix = "" + if self._hourly: + name_appendix = " Hourly" if name is not None: - return f"{name}{self._name_appendix}" + return f"{name}{name_appendix}" if self.track_home: - return f"{self.hass.config.location_name}{self._name_appendix}" + return f"{self.hass.config.location_name}{name_appendix}" - return f"{DEFAULT_NAME}{self._name_appendix}" + return f"{DEFAULT_NAME}{name_appendix}" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return not self._hourly @property def condition(self): diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 064335a998c..242352c2498 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -1,13 +1,27 @@ """Test Met weather entity.""" +from homeassistant.components.met import DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN + async def test_tracking_home(hass, mock_weather): """Test we track home.""" await hass.config_entries.flow.async_init("met", context={"source": "onboarding"}) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 2 + assert len(hass.states.async_entity_ids("weather")) == 1 assert len(mock_weather.mock_calls) == 4 + # Test the hourly sensor is disabled by default + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("weather.test_home_hourly") + assert state is None + + entry = registry.async_get("weather.test_home_hourly") + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" + # Test we track config await hass.config.async_update(latitude=10, longitude=20) await hass.async_block_till_done() @@ -21,6 +35,17 @@ async def test_tracking_home(hass, mock_weather): async def test_not_tracking_home(hass, mock_weather): """Test when we not track home.""" + + # Pre-create registry entry for disabled by default hourly weather + registry = await hass.helpers.entity_registry.async_get_registry() + registry.async_get_or_create( + WEATHER_DOMAIN, + DOMAIN, + "10-20-hourly", + suggested_object_id="somewhere_hourly", + disabled_by=None, + ) + await hass.config_entries.flow.async_init( "met", context={"source": "user"}, diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 8b8f9761bdb..0d425642622 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -276,4 +276,4 @@ async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client): assert resp.status == 200 await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("weather")) == 2 + assert len(hass.states.async_entity_ids("weather")) == 1 From 081bd22e596252ac1efe619082e4c0f118a570ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 10 Sep 2020 00:32:49 +0200 Subject: [PATCH 775/862] Updated frontend to 20200909.0 (#39869) Co-authored-by: Bram Kragten --- 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 379e08d30a7..7ccb606894a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200908.0"], + "requirements": ["home-assistant-frontend==20200909.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 99654fafb68..300d56f5d47 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.36.1 -home-assistant-frontend==20200908.0 +home-assistant-frontend==20200909.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2490e6b366d..7b0cccef3db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200908.0 +home-assistant-frontend==20200909.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 618facba40a..696bed5c751 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200908.0 +home-assistant-frontend==20200909.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From f0295d562d4a0320eb2441979e10dd5ca28f4c10 Mon Sep 17 00:00:00 2001 From: bsmappee <58250533+bsmappee@users.noreply.github.com> Date: Thu, 10 Sep 2020 10:27:07 +0200 Subject: [PATCH 776/862] Bump pysmappee to 0.2.13 (#39883) --- homeassistant/components/smappee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 1d918c06cc0..c6cac118b72 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.2.10" + "pysmappee==0.2.13" ], "codeowners": [ "@bsmappee" diff --git a/requirements_all.txt b/requirements_all.txt index 7b0cccef3db..a9e551418d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1632,7 +1632,7 @@ pyskyqhub==0.1.3 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.10 +pysmappee==0.2.13 # homeassistant.components.smartthings pysmartapp==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 696bed5c751..f4f16a9aff5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ pysignalclirestapi==0.3.4 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.10 +pysmappee==0.2.13 # homeassistant.components.smartthings pysmartapp==0.3.2 From 36f52a26f625a7d168ed69b1bf844de27a30927c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 11:25:56 +0200 Subject: [PATCH 777/862] Fix event trigger (#39884) Co-authored-by: Franck Nijhof --- .../components/homeassistant/triggers/event.py | 14 +++++++++----- .../homeassistant/triggers/test_event.py | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index 8fa9207c3b0..800d6bb5f77 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -28,11 +28,15 @@ async def async_attach_trigger( ): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) - event_data_schema = ( - vol.Schema(config.get(CONF_EVENT_DATA), extra=vol.ALLOW_EXTRA) - if config.get(CONF_EVENT_DATA) - else None - ) + event_data_schema = None + if config.get(CONF_EVENT_DATA): + event_data_schema = vol.Schema( + { + vol.Required(key): value + for key, value in config.get(CONF_EVENT_DATA).items() + }, + extra=vol.ALLOW_EXTRA, + ) @callback def handle_event(event): diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index cc9aecfdac4..84b7e725f0a 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -84,17 +84,27 @@ async def test_if_fires_on_event_with_data(hass, calls): "trigger": { "platform": "event", "event_type": "test_event", - "event_data": {"some_attr": "some_value"}, + "event_data": { + "some_attr": "some_value", + "second_attr": "second_value", + }, }, "action": {"service": "test.automation"}, } }, ) - hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) + hass.bus.async_fire( + "test_event", + {"some_attr": "some_value", "another": "value", "second_attr": "second_value"}, + ) await hass.async_block_till_done() assert len(calls) == 1 + hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) + await hass.async_block_till_done() + assert len(calls) == 1 # No new call + async def test_if_fires_on_event_with_empty_data_config(hass, calls): """Test the firing of events with empty data config. From b26ab2849b63d8a4a8e89247e7e75524ec4396d4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 Sep 2020 11:18:43 +0200 Subject: [PATCH 778/862] Bump hass-nabucasa 0.37.0 (#39885) --- homeassistant/components/cloud/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/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 63afb402b9f..2da3562717d 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.36.1"], + "requirements": ["hass-nabucasa==0.37.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 300d56f5d47..53f22d3787b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 -hass-nabucasa==0.36.1 +hass-nabucasa==0.37.0 home-assistant-frontend==20200909.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 diff --git a/requirements_all.txt b/requirements_all.txt index a9e551418d8..b875fa5e4a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -720,7 +720,7 @@ habitipy==0.2.0 hangups==0.4.10 # homeassistant.components.cloud -hass-nabucasa==0.36.1 +hass-nabucasa==0.37.0 # homeassistant.components.jewish_calendar hdate==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4f16a9aff5..69b6d51416f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,7 +352,7 @@ ha-ffmpeg==2.0 hangups==0.4.10 # homeassistant.components.cloud -hass-nabucasa==0.36.1 +hass-nabucasa==0.37.0 # homeassistant.components.jewish_calendar hdate==0.9.5 From 896df60f3260815cb03f70b2a29ecad6ce73d1c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 12:08:17 +0200 Subject: [PATCH 779/862] Shelly switch to guard for shelly 2 in roller mode (#39886) --- homeassistant/components/shelly/switch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 1c3c48637e9..5550240478f 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -13,6 +13,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up switches for device.""" wrapper = hass.data[DOMAIN][config_entry.entry_id] + # In roller mode the relay blocks exist but do not contain required info + if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": + return + relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"] if not relay_blocks: From 5098c3581474959dc76a8f7442972976e269dcbe Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Sep 2020 12:06:18 +0200 Subject: [PATCH 780/862] Fix spotify media browser category (#39888) --- homeassistant/components/spotify/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index eb05fcc4ddc..e2850027229 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -15,6 +15,7 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_EPISODE, + MEDIA_CLASS_GENRE, MEDIA_CLASS_PLAYLIST, MEDIA_CLASS_PODCAST, MEDIA_CLASS_TRACK, @@ -113,7 +114,7 @@ CONTENT_TYPE_MEDIA_CLASS = { "current_user_top_artists": MEDIA_CLASS_ARTIST, "current_user_top_tracks": MEDIA_CLASS_TRACK, "featured_playlists": MEDIA_CLASS_PLAYLIST, - "categories": MEDIA_CLASS_DIRECTORY, + "categories": MEDIA_CLASS_GENRE, "category_playlists": MEDIA_CLASS_PLAYLIST, "new_releases": MEDIA_CLASS_ALBUM, MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, @@ -566,6 +567,7 @@ def build_item_response(spotify, user, payload): can_expand=True, ) ) + return media_item if title is None: if "name" in media: From b7dacabbe40ed6035bc2dc1759bb63e714ff499b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 Sep 2020 15:56:05 +0200 Subject: [PATCH 781/862] Fix issue with grpcio build on 32bit arch (#39893) --- azure-pipelines-wheels.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 3e7821d77af..ac5f4fd824f 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -49,6 +49,7 @@ jobs: builderVersion: '$(versionWheels)' builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' builderPip: 'Cython;numpy;scikit-build' + builderEnvFile: true skipBinary: 'aiohttp' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' @@ -90,4 +91,10 @@ jobs: sed -i "s|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} done + + # Write env for build settings + ( + echo "GRPC_BUILD_WITH_BORING_SSL_ASM=0" + echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1" + ) > .env_file displayName: 'Prepare requirements files for Home Assistant wheels' From 209cf44e8e38f042236cdb01e9c1d2f312016145 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 20:41:42 +0200 Subject: [PATCH 782/862] Add default variables to script helper (#39895) --- .../components/automation/__init__.py | 27 +++++++- homeassistant/components/script/__init__.py | 5 +- homeassistant/const.py | 1 + homeassistant/helpers/config_validation.py | 3 + homeassistant/helpers/script.py | 26 +++++-- homeassistant/helpers/template.py | 17 ++++- homeassistant/helpers/typing.py | 4 +- tests/components/automation/test_init.py | 54 +++++++++++++++ tests/components/script/test_init.py | 68 ++++++++++++++++++- 9 files changed, 190 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index db97c3a321a..392ca710000 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_ID, CONF_MODE, CONF_PLATFORM, + CONF_VARIABLES, CONF_ZONE, EVENT_HOMEASSISTANT_STARTED, SERVICE_RELOAD, @@ -29,7 +30,7 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import condition, extract_domain_configs +from homeassistant.helpers import condition, extract_domain_configs, template import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -104,6 +105,7 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_HIDE_ENTITY): cv.boolean, vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA, vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, + vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, }, SCRIPT_MODE_SINGLE, @@ -239,6 +241,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): cond_func, action_script, initial_state, + variables, ): """Initialize an automation entity.""" self._id = automation_id @@ -253,6 +256,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._referenced_entities: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None self._logger = _LOGGER + self._variables = variables + self._variables_dynamic = template.is_complex(variables) @property def name(self): @@ -329,6 +334,9 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Startup with initial state or previous state.""" await super().async_added_to_hass() + if self._variables_dynamic: + template.attach(cast(HomeAssistant, self.hass), self._variables) + self._logger = logging.getLogger( f"{__name__}.{split_entity_id(self.entity_id)[1]}" ) @@ -378,11 +386,22 @@ class AutomationEntity(ToggleEntity, RestoreEntity): else: await self.async_disable() - async def async_trigger(self, variables, context=None, skip_condition=False): + async def async_trigger(self, run_variables, context=None, skip_condition=False): """Trigger automation. This method is a coroutine. """ + if self._variables: + if self._variables_dynamic: + variables = template.render_complex(self._variables, run_variables) + else: + variables = dict(self._variables) + else: + variables = {} + + if run_variables: + variables.update(run_variables) + if ( not skip_condition and self._cond_func is not None @@ -518,6 +537,9 @@ async def _async_process_config(hass, config, component): max_runs=config_block[CONF_MAX], max_exceeded=config_block[CONF_MAX_EXCEEDED], logger=_LOGGER, + # We don't pass variables here + # Automation will already render them to use them in the condition + # and so will pass them on to the script. ) if CONF_CONDITION in config_block: @@ -535,6 +557,7 @@ async def _async_process_config(hass, config, component): cond_func, action_script, initial_state, + config_block.get(CONF_VARIABLES), ) entities.append(entity) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 20f12361621..1e0fad9be5d 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_ICON, CONF_MODE, CONF_SEQUENCE, + CONF_VARIABLES, SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF, @@ -59,6 +60,7 @@ SCRIPT_ENTRY_SCHEMA = make_script_schema( vol.Optional(CONF_ICON): cv.icon, vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, vol.Optional(CONF_DESCRIPTION, default=""): cv.string, + vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA, vol.Optional(CONF_FIELDS, default={}): { cv.string: { vol.Optional(CONF_DESCRIPTION): cv.string, @@ -75,7 +77,7 @@ CONFIG_SCHEMA = vol.Schema( SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( - {vol.Optional(ATTR_VARIABLES): dict} + {vol.Optional(ATTR_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA} ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -263,6 +265,7 @@ class ScriptEntity(ToggleEntity): max_runs=cfg[CONF_MAX], max_exceeded=cfg[CONF_MAX_EXCEEDED], logger=logging.getLogger(f"{__name__}.{object_id}"), + variables=cfg.get(CONF_VARIABLES), ) self._changed = asyncio.Event() diff --git a/homeassistant/const.py b/homeassistant/const.py index a20d0effdaf..465f93c3f76 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -179,6 +179,7 @@ CONF_UNTIL = "until" CONF_URL = "url" CONF_USERNAME = "username" CONF_VALUE_TEMPLATE = "value_template" +CONF_VARIABLES = "variables" CONF_VERIFY_SSL = "verify_ssl" CONF_WAIT_FOR_TRIGGER = "wait_for_trigger" CONF_WAIT_TEMPLATE = "wait_template" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index c3842c538d8..a54f97ec7e5 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -863,6 +863,9 @@ def make_entity_service_schema( ) +SCRIPT_VARIABLES_SCHEMA = vol.Schema({str: template_complex}) + + def script_action(value: Any) -> dict: """Validate a script action.""" if not isinstance(value, dict): diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 74660f8b391..cd664974431 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -53,11 +53,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, ) from homeassistant.core import SERVICE_CALL_LIMIT, Context, HomeAssistant, callback -from homeassistant.helpers import ( - condition, - config_validation as cv, - template as template, -) +from homeassistant.helpers import condition, config_validation as cv, template from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.service import ( CONF_SERVICE_DATA, @@ -721,6 +717,7 @@ class Script: logger: Optional[logging.Logger] = None, log_exceptions: bool = True, top_level: bool = True, + variables: Optional[Dict[str, Any]] = None, ) -> None: """Initialize the script.""" all_scripts = hass.data.get(DATA_SCRIPTS) @@ -759,6 +756,10 @@ class Script: self._choose_data: Dict[int, Dict[str, Any]] = {} self._referenced_entities: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None + self.variables = variables + self._variables_dynamic = template.is_complex(variables) + if self._variables_dynamic: + template.attach(hass, variables) def _set_logger(self, logger: Optional[logging.Logger] = None) -> None: if logger: @@ -867,7 +868,7 @@ class Script: async def async_run( self, - variables: Optional[_VarsType] = None, + run_variables: Optional[_VarsType] = None, context: Optional[Context] = None, started_action: Optional[Callable[..., Any]] = None, ) -> None: @@ -898,8 +899,19 @@ class Script: # are read-only, but more importantly, so as not to leak any variables created # during the run back to the caller. if self._top_level: - variables = dict(variables) if variables is not None else {} + if self.variables: + if self._variables_dynamic: + variables = template.render_complex(self.variables, run_variables) + else: + variables = dict(self.variables) + else: + variables = {} + + if run_variables: + variables.update(run_variables) variables["context"] = context + else: + variables = cast(dict, run_variables) if self.script_mode != SCRIPT_MODE_QUEUED: cls = _ScriptRun diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index c771992caa4..917581fac07 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -65,7 +65,7 @@ def attach(hass: HomeAssistantType, obj: Any) -> None: if isinstance(obj, list): for child in obj: attach(hass, child) - elif isinstance(obj, dict): + elif isinstance(obj, collections.abc.Mapping): for child_key, child_value in obj.items(): attach(hass, child_key) attach(hass, child_value) @@ -77,7 +77,7 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: """Recursive template creator helper function.""" if isinstance(value, list): return [render_complex(item, variables) for item in value] - if isinstance(value, dict): + if isinstance(value, collections.abc.Mapping): return { render_complex(key, variables): render_complex(item, variables) for key, item in value.items() @@ -88,6 +88,19 @@ def render_complex(value: Any, variables: TemplateVarsType = None) -> Any: return value +def is_complex(value: Any) -> bool: + """Test if data structure is a complex template.""" + if isinstance(value, Template): + return True + if isinstance(value, list): + return any(is_complex(val) for val in value) + if isinstance(value, collections.abc.Mapping): + return any(is_complex(val) for val in value.keys()) or any( + is_complex(val) for val in value.values() + ) + return False + + def is_template_string(maybe_template: str) -> bool: """Check if the input is a Jinja2 template.""" return _RE_JINJA_DELIMITERS.search(maybe_template) is not None diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 6bcc98c10a8..bed0d2b8d17 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,5 +1,5 @@ """Typing Helpers for Home Assistant.""" -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, Mapping, Optional, Tuple, Union import homeassistant.core @@ -12,7 +12,7 @@ HomeAssistantType = homeassistant.core.HomeAssistant ServiceCallType = homeassistant.core.ServiceCall ServiceDataType = Dict[str, Any] StateType = Union[None, str, int, float] -TemplateVarsType = Optional[Dict[str, Any]] +TemplateVarsType = Optional[Mapping[str, Any]] # Custom type for recorder Queries QueryType = Any diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 3952e781952..5ee0ff62af2 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1134,3 +1134,57 @@ async def test_logbook_humanify_automation_triggered_event(hass): assert event2["domain"] == "automation" assert event2["message"] == "has been triggered by source of trigger" assert event2["entity_id"] == "automation.bye" + + +async def test_automation_variables(hass): + """Test automation variables.""" + calls = async_mock_service(hass, "test", "automation") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "variables": { + "test_var": "defined_in_config", + "event_type": "{{ trigger.event.event_type }}", + }, + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": { + "service": "test.automation", + "data": { + "value": "{{ test_var }}", + "event_type": "{{ event_type }}", + }, + }, + }, + { + "variables": { + "test_var": "defined_in_config", + }, + "trigger": {"platform": "event", "event_type": "test_event_2"}, + "condition": { + "condition": "template", + "value_template": "{{ trigger.event.data.pass_condition }}", + }, + "action": { + "service": "test.automation", + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["value"] == "defined_in_config" + assert calls[0].data["event_type"] == "test_event" + + hass.bus.async_fire("test_event_2") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.bus.async_fire("test_event_2", {"pass_condition": True}) + await hass.async_block_till_done() + assert len(calls) == 2 diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 22625d46530..5fb832d0f36 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -23,7 +23,7 @@ from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component, setup_component from tests.async_mock import Mock, patch -from tests.common import get_test_home_assistant +from tests.common import async_mock_service, get_test_home_assistant from tests.components.logbook.test_init import MockLazyEventPartialState ENTITY_ID = "script.test" @@ -615,3 +615,69 @@ async def test_concurrent_script(hass, concurrently): assert not script.is_on(hass, "script.script1") assert not script.is_on(hass, "script.script2") + + +async def test_script_variables(hass): + """Test defining scripts.""" + assert await async_setup_component( + hass, + "script", + { + "script": { + "script1": { + "variables": { + "test_var": "from_config", + "templated_config_var": "{{ var_from_service | default('config-default') }}", + }, + "sequence": [ + { + "service": "test.script", + "data": { + "value": "{{ test_var }}", + "templated_config_var": "{{ templated_config_var }}", + }, + }, + ], + }, + "script2": { + "variables": { + "test_var": "from_config", + }, + "sequence": [ + { + "service": "test.script", + "data": { + "value": "{{ test_var }}", + }, + }, + ], + }, + } + }, + ) + + mock_calls = async_mock_service(hass, "test", "script") + + await hass.services.async_call( + "script", "script1", {"var_from_service": "hello"}, blocking=True + ) + + assert len(mock_calls) == 1 + assert mock_calls[0].data["value"] == "from_config" + assert mock_calls[0].data["templated_config_var"] == "hello" + + await hass.services.async_call( + "script", "script1", {"test_var": "from_service"}, blocking=True + ) + + assert len(mock_calls) == 2 + assert mock_calls[1].data["value"] == "from_service" + assert mock_calls[1].data["templated_config_var"] == "config-default" + + # Call script with vars but no templates in it + await hass.services.async_call( + "script", "script2", {"test_var": "from_service"}, blocking=True + ) + + assert len(mock_calls) == 3 + assert mock_calls[2].data["value"] == "from_service" From 7370b0ffc66adb041eda05ded626e7edf2ab619b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 13:50:11 -0500 Subject: [PATCH 783/862] Detect self-referencing loops in template entities and log a warning (#39897) --- .../components/template/template_entity.py | 8 ++++ homeassistant/helpers/event.py | 16 +++++++ tests/components/template/test_sensor.py | 44 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 20b0caec3ca..2e42b28fbd2 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -121,6 +121,7 @@ class TemplateEntity(Entity): """Template Entity.""" self._template_attrs = {} self._async_update = None + self._async_update_entity_ids_filter = None self._attribute_templates = attribute_templates self._attributes = {} self._availability_template = availability_template @@ -231,6 +232,9 @@ class TemplateEntity(Entity): event, update.template, update.last_result, update.result ) + if self._async_update_entity_ids_filter: + self._async_update_entity_ids_filter({self.entity_id}) + if self._async_update: self.async_write_ha_state() @@ -245,8 +249,12 @@ class TemplateEntity(Entity): ) self.async_on_remove(result_info.async_remove) result_info.async_refresh() + result_info.async_update_entity_ids_filter({self.entity_id}) self.async_write_ha_state() self._async_update = result_info.async_refresh + self._async_update_entity_ids_filter = ( + result_info.async_update_entity_ids_filter + ) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index dcc05675a01..d9f1b8d9681 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -508,6 +508,7 @@ class _TrackTemplateResultInfo: self._info: Dict[Template, RenderInfo] = {} self._last_domains: Set = set() self._last_entities: Set = set() + self._entity_ids_filter: Set = set() def async_setup(self) -> None: """Activation of template tracking.""" @@ -659,12 +660,27 @@ class _TrackTemplateResultInfo: """Force recalculate the template.""" self._refresh(None) + @callback + def async_update_entity_ids_filter(self, entity_ids: Set) -> None: + """Update the filtered entity_ids.""" + self._entity_ids_filter = entity_ids + @callback def _refresh(self, event: Optional[Event]) -> None: entity_id = event and event.data.get(ATTR_ENTITY_ID) updates = [] info_changed = False + if entity_id and entity_id in self._entity_ids_filter: + # Skip self-referencing updates + for track_template_ in self._track_templates: + _LOGGER.warning( + "Template loop detected while processing event: %s, skipping template render for Template[%s]", + event, + track_template_.template.template, + ) + return + for track_template_ in self._track_templates: template = track_template_.template if ( diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 31b298330e8..439f154b4af 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -758,3 +758,47 @@ async def test_sun_renders_once_per_sensor(hass): "{{ state_attr('sun.sun', 'elevation') }}", "{{ state_attr('sun.sun', 'next_rising') }}", } + + +async def test_self_referencing_sensor_loop(hass, caplog): + """Test a self referencing sensor does not loop forever.""" + + template_str = """ +{% for state in states -%} + {{ state.last_updated }} +{%- endfor %} +""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": template_str, + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + value = hass.states.get("sensor.test").state + await hass.async_block_till_done() + + value2 = hass.states.get("sensor.test").state + assert value2 == value + + await hass.async_block_till_done() + + value3 = hass.states.get("sensor.test").state + assert value3 == value2 + + assert "Template loop detected" in caplog.text From b8ef87d84ceb554b65c51748a6c60a8f0318eb86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 11:10:43 -0500 Subject: [PATCH 784/862] Fix ping log level to be debug instead of warning (#39900) --- homeassistant/components/ping/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index 5befe0b7f3a..ac73da0a13f 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -134,7 +134,7 @@ class PingDataICMPLib(PingData): async def async_update(self) -> None: """Retrieve the latest details from the host.""" - _LOGGER.warning("ping address: %s", self._ip_address) + _LOGGER.debug("ping address: %s", self._ip_address) data = await self.hass.async_add_executor_job( partial( icmp_ping, From 6f8060dea77b3484771a38467626348b16fc1932 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Sep 2020 20:52:23 +0200 Subject: [PATCH 785/862] Fix discovery update of MQTT state templates (#39901) --- homeassistant/components/mqtt/__init__.py | 2 +- .../components/mqtt/alarm_control_panel.py | 25 +- .../components/mqtt/binary_sensor.py | 25 +- homeassistant/components/mqtt/cover.py | 18 +- homeassistant/components/mqtt/fan.py | 31 +- .../components/mqtt/light/schema_basic.py | 28 +- homeassistant/components/mqtt/lock.py | 17 +- homeassistant/components/mqtt/sensor.py | 27 +- homeassistant/components/mqtt/switch.py | 17 +- .../mqtt/test_alarm_control_panel.py | 58 ++- tests/components/mqtt/test_binary_sensor.py | 62 ++- tests/components/mqtt/test_common.py | 42 +- tests/components/mqtt/test_cover.py | 8 + tests/components/mqtt/test_light.py | 471 +++++++++++++++++- tests/components/mqtt/test_sensor.py | 68 ++- tests/components/mqtt/test_switch.py | 82 ++- 16 files changed, 853 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index d4263ee6ba3..2b5dca6474f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1305,7 +1305,7 @@ class MqttDiscoveryUpdate(Entity): debug_info.add_entity_discovery_data( self.hass, self._discovery_data, self.entity_id ) - # Set in case the entity has been removed and is re-added + # Set in case the entity has been removed and is re-added, for example when changing entity_id set_discovery_hash(self.hass, discovery_hash) self._remove_signal = async_dispatcher_connect( self.hass, diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 505c7616a0a..6d33175e6ca 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -104,7 +104,7 @@ async def async_setup_platform( ): """Set up MQTT alarm control panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -116,7 +116,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -128,10 +128,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT Alarm Control Panel platform.""" - async_add_entities([MqttAlarm(config, config_entry, discovery_data)]) + async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) class MqttAlarm( @@ -143,13 +143,16 @@ class MqttAlarm( ): """Representation of a MQTT alarm status.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Init the MQTT Alarm Control Panel.""" + self.hass = hass self._state = None - self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) self._sub_state = None + # Load config + self._setup_from_config(config) + device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -165,26 +168,30 @@ class MqttAlarm( async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) - self._config = config + self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" + def _setup_from_config(self, config): + self._config = config value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass command_template = self._config[CONF_COMMAND_TEMPLATE] command_template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback @log_messages(self.hass, self.entity_id) def message_received(msg): """Run when new MQTT message has been received.""" payload = msg.payload + value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: payload = value_template.async_render_with_possible_json_value( msg.payload, self._state diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index c9fd2bba2b1..cf7b0fc3ca6 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -76,7 +76,7 @@ async def async_setup_platform( ): """Set up MQTT binary sensor through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -88,7 +88,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -100,10 +100,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT binary sensor.""" - async_add_entities([MqttBinarySensor(config, config_entry, discovery_data)]) + async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) class MqttBinarySensor( @@ -115,9 +115,9 @@ class MqttBinarySensor( ): """Representation a binary sensor that is updated by MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT binary sensor.""" - self._config = config + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None self._sub_state = None @@ -128,6 +128,10 @@ class MqttBinarySensor( self._expired = True else: self._expired = None + + # Load config + self._setup_from_config(config) + device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -143,19 +147,22 @@ class MqttBinarySensor( async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) - self._config = config + self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" + def _setup_from_config(self, config): + self._config = config value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback def off_delay_listener(now): """Switch device off after a delay.""" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index b8a5b778a98..20146b0b7d6 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -174,7 +174,7 @@ async def async_setup_platform( ): """Set up MQTT cover through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -186,7 +186,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -198,10 +198,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT Cover.""" - async_add_entities([MqttCover(config, config_entry, discovery_data)]) + async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) class MqttCover( @@ -213,8 +213,9 @@ class MqttCover( ): """Representation of a cover that can be controlled using MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the cover.""" + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._position = None self._state = None @@ -257,8 +258,6 @@ class MqttCover( ) self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC] - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: template.hass = self.hass @@ -269,6 +268,8 @@ class MqttCover( if tilt_status_template is not None: tilt_status_template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" topics = {} @callback @@ -276,6 +277,7 @@ class MqttCover( def tilt_message_received(msg): """Handle tilt updates.""" payload = msg.payload + tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE) if tilt_status_template is not None: payload = tilt_status_template.async_render_with_possible_json_value( payload @@ -296,6 +298,7 @@ class MqttCover( def state_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload + template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: payload = template.async_render_with_possible_json_value(payload) @@ -321,6 +324,7 @@ class MqttCover( def position_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload + template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: payload = template.async_render_with_possible_json_value(payload) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index b1ec7aabeef..14469e415e0 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -115,7 +115,7 @@ async def async_setup_platform( ): """Set up MQTT fan through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -127,7 +127,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -139,10 +139,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT fan.""" - async_add_entities([MqttFan(config, config_entry, discovery_data)]) + async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) class MqttFan( @@ -154,8 +154,9 @@ class MqttFan( ): """A MQTT fan component.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT fan.""" + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._state = False self._speed = None @@ -242,22 +243,22 @@ class MqttFan( self._topic[CONF_SPEED_COMMAND_TOPIC] is not None and SUPPORT_SET_SPEED ) + for key, tpl in list(self._templates.items()): + if tpl is None: + self._templates[key] = lambda value: value + else: + tpl.hass = self.hass + self._templates[key] = tpl.async_render_with_possible_json_value + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} - templates = {} - for key, tpl in list(self._templates.items()): - if tpl is None: - templates[key] = lambda value: value - else: - tpl.hass = self.hass - templates[key] = tpl.async_render_with_possible_json_value @callback @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new received MQTT message.""" - payload = templates[CONF_STATE](msg.payload) + payload = self._templates[CONF_STATE](msg.payload) if payload == self._payload["STATE_ON"]: self._state = True elif payload == self._payload["STATE_OFF"]: @@ -275,7 +276,7 @@ class MqttFan( @log_messages(self.hass, self.entity_id) def speed_received(msg): """Handle new received MQTT message for the speed.""" - payload = templates[ATTR_SPEED](msg.payload) + payload = self._templates[ATTR_SPEED](msg.payload) if payload == self._payload["SPEED_LOW"]: self._speed = SPEED_LOW elif payload == self._payload["SPEED_MEDIUM"]: @@ -298,7 +299,7 @@ class MqttFan( @log_messages(self.hass, self.entity_id) def oscillation_received(msg): """Handle new received MQTT message for the oscillation.""" - payload = templates[OSCILLATION](msg.payload) + payload = self._templates[OSCILLATION](msg.payload) if payload == self._payload["OSCILLATE_ON_PAYLOAD"]: self._oscillation = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index e19fcbf0e40..d7c373bf72b 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -254,7 +254,7 @@ class MqttLight( value_templates = {} for key in VALUE_TEMPLATE_KEYS: - value_templates[key] = lambda value: value + value_templates[key] = lambda value, _: value for key in VALUE_TEMPLATE_KEYS & config.keys(): tpl = config[key] value_templates[key] = tpl.async_render_with_possible_json_value @@ -304,7 +304,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new MQTT messages.""" - payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty state message from '%s'", msg.topic) return @@ -328,7 +330,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def brightness_received(msg): """Handle new MQTT messages for the brightness.""" - payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty brightness message from '%s'", msg.topic) return @@ -360,7 +364,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def rgb_received(msg): """Handle new MQTT messages for RGB.""" - payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload, None) if not payload: _LOGGER.debug("Ignoring empty rgb message from '%s'", msg.topic) return @@ -392,7 +396,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def color_temp_received(msg): """Handle new MQTT messages for color temperature.""" - payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic) return @@ -422,7 +428,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def effect_received(msg): """Handle new MQTT messages for effect.""" - payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty effect message from '%s'", msg.topic) return @@ -452,7 +460,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def hs_received(msg): """Handle new MQTT messages for hs color.""" - payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload, None) if not payload: _LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic) return @@ -484,7 +492,9 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def white_value_received(msg): """Handle new MQTT messages for white value.""" - payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE]( + msg.payload, None + ) if not payload: _LOGGER.debug("Ignoring empty white value message from '%s'", msg.topic) return @@ -516,7 +526,7 @@ class MqttLight( @log_messages(self.hass, self.entity_id) def xy_received(msg): """Handle new MQTT messages for xy color.""" - payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload) + payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload, None) if not payload: _LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic) return diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index f6d56a30431..aea1e40b0f9 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -77,7 +77,7 @@ async def async_setup_platform( ): """Set up MQTT lock panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -89,7 +89,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -101,10 +101,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT Lock platform.""" - async_add_entities([MqttLock(config, config_entry, discovery_data)]) + async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) class MqttLock( @@ -116,8 +116,9 @@ class MqttLock( ): """Representation of a lock that can be toggled using MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the lock.""" + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._state = False self._sub_state = None @@ -154,17 +155,19 @@ class MqttLock( self._optimistic = config[CONF_OPTIMISTIC] - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" payload = msg.payload + value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: payload = value_template.async_render_with_possible_json_value(payload) if payload == self._config[CONF_STATE_LOCKED]: diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index eb241bba7a0..ffd34cef8c9 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -70,7 +70,7 @@ async def async_setup_platform( ): """Set up MQTT sensors through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -94,10 +94,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config: ConfigType, async_add_entities, config_entry=None, discovery_data=None + hass, config: ConfigType, async_add_entities, config_entry=None, discovery_data=None ): """Set up MQTT sensor.""" - async_add_entities([MqttSensor(config, config_entry, discovery_data)]) + async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) class MqttSensor( @@ -105,9 +105,9 @@ class MqttSensor( ): """Representation of a sensor that can be updated using MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the sensor.""" - self._config = config + self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None self._sub_state = None @@ -118,6 +118,10 @@ class MqttSensor( self._expired = True else: self._expired = None + + # Load config + self._setup_from_config(config) + device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -133,19 +137,23 @@ class MqttSensor( async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) - self._config = config + self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + self._config = config template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback @log_messages(self.hass, self.entity_id) def message_received(msg): @@ -169,6 +177,7 @@ class MqttSensor( self.hass, self._value_is_expired, expiration_at ) + template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: payload = template.async_render_with_possible_json_value( payload, self._state diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 364f060ac14..761f19ef054 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -73,7 +73,7 @@ async def async_setup_platform( ): """Set up MQTT switch through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities, discovery_info) + await _async_setup_entity(hass, config, async_add_entities, discovery_info) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -85,7 +85,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data + hass, config, async_add_entities, config_entry, discovery_data ) except Exception: clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) @@ -97,10 +97,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + hass, config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT switch.""" - async_add_entities([MqttSwitch(config, config_entry, discovery_data)]) + async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) class MqttSwitch( @@ -113,8 +113,9 @@ class MqttSwitch( ): """Representation of a switch that can be toggled using MQTT.""" - def __init__(self, config, config_entry, discovery_data): + def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" + self.hass = hass self._state = False self._sub_state = None @@ -160,17 +161,19 @@ class MqttSwitch( self._optimistic = config[CONF_OPTIMISTIC] - async def _subscribe_topics(self): - """(Re)Subscribe to topics.""" template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: template.hass = self.hass + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + @callback @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle new MQTT state messages.""" payload = msg.payload + template = self._config.get(CONF_VALUE_TEMPLATE) if template is not None: payload = template.async_render_with_possible_json_value(payload) if payload == self._state_on: diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 30ec5316399..7eb890903fd 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -603,17 +603,71 @@ async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): ) -async def test_discovery_update_alarm(hass, mqtt_mock, caplog): +async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" + config1["state_topic"] = "alarm/state1" + config2["state_topic"] = "alarm/state2" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("alarm/state1", '{"state1":{"state":"armed_away"}}')], "armed_away", None), + ] + state_data2 = [ + ([("alarm/state1", '{"state1":{"state":"triggered"}}')], "armed_away", None), + ([("alarm/state1", '{"state2":{"state":"triggered"}}')], "armed_away", None), + ([("alarm/state2", '{"state1":{"state":"triggered"}}')], "armed_away", None), + ([("alarm/state2", '{"state2":{"state":"triggered"}}')], "triggered", None), + ] data1 = json.dumps(config1) data2 = json.dumps(config2) await help_test_discovery_update( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, data2 + hass, + mqtt_mock, + caplog, + alarm_control_panel.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): + """Test update of discovered alarm_control_panel.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "alarm/state1" + config2["state_topic"] = "alarm/state1" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("alarm/state1", '{"state1":{"state":"armed_away"}}')], "armed_away", None), + ] + state_data2 = [ + ([("alarm/state1", '{"state1":{"state":"triggered"}}')], "armed_away", None), + ([("alarm/state1", '{"state2":{"state":"triggered"}}')], "triggered", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + alarm_control_panel.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index c739f4378d1..d2375278c4d 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -580,17 +580,75 @@ async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): ) -async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, caplog): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config1["name"] = "Beer" config2["name"] = "Milk" + config1["state_topic"] = "sensor/state1" + config2["state_topic"] = "sensor/state2" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("sensor/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("sensor/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ([("sensor/state2", '{"state2":{"state":"ON"}}')], "on", None), + ([("sensor/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("sensor/state1", '{"state2":{"state":"OFF"}}')], "on", None), + ([("sensor/state2", '{"state1":{"state":"OFF"}}')], "on", None), + ([("sensor/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ] data1 = json.dumps(config1) data2 = json.dumps(config2) await help_test_discovery_update( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, data2 + hass, + mqtt_mock, + caplog, + binary_sensor.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): + """Test update of discovered binary_sensor.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "sensor/state1" + config2["state_topic"] = "sensor/state1" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("sensor/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("sensor/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ([("sensor/state1", '{"state2":{"state":"ON"}}')], "on", None), + ([("sensor/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("sensor/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + binary_sensor.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 75e1c12a46c..27ee060ebfa 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -512,10 +512,6 @@ async def help_test_discovery_update( discovery_data2, state_data1=None, state_data2=None, - state1=None, - state2=None, - attributes1=None, - attributes2=None, ): """Test update of discovered component. @@ -527,32 +523,38 @@ async def help_test_discovery_update( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data1) await hass.async_block_till_done() - if state_data1: - for (topic, data) in state_data1: - async_fire_mqtt_message(hass, topic, data) state = hass.states.get(f"{domain}.beer") assert state is not None assert state.name == "Beer" - if state1: - assert state.state == state1 - if attributes1: - for (attr, value) in attributes1: - assert state.attributes.get(attr) == value + + if state_data1: + for (mqtt_messages, expected_state, attributes) in state_data1: + for (topic, data) in mqtt_messages: + async_fire_mqtt_message(hass, topic, data) + state = hass.states.get(f"{domain}.beer") + if expected_state: + assert state.state == expected_state + if attributes: + for (attr, value) in attributes: + assert state.attributes.get(attr) == value async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data2) await hass.async_block_till_done() - if state_data2: - for (topic, data) in state_data2: - async_fire_mqtt_message(hass, topic, data) state = hass.states.get(f"{domain}.beer") assert state is not None assert state.name == "Milk" - if state2: - assert state.state == state2 - if attributes2: - for (attr, value) in attributes2: - assert state.attributes.get(attr) == value + + if state_data2: + for (mqtt_messages, expected_state, attributes) in state_data2: + for (topic, data) in mqtt_messages: + async_fire_mqtt_message(hass, topic, data) + state = hass.states.get(f"{domain}.beer") + if expected_state: + assert state.state == expected_state + if attributes: + for (attr, value) in attributes: + assert state.attributes.get(attr) == value state = hass.states.get(f"{domain}.milk") assert state is None diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index f9036bcfa0f..d1529e63fc7 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1397,6 +1397,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): async def test_find_percentage_in_range_defaults(hass, mqtt_mock): """Test find percentage in range with default range.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1440,6 +1441,7 @@ async def test_find_percentage_in_range_defaults(hass, mqtt_mock): async def test_find_percentage_in_range_altered(hass, mqtt_mock): """Test find percentage in range with altered range.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1483,6 +1485,7 @@ async def test_find_percentage_in_range_altered(hass, mqtt_mock): async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): """Test find percentage in range with default range but inverted.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1526,6 +1529,7 @@ async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): """Test find percentage in range with altered range and inverted.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1569,6 +1573,7 @@ async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): async def test_find_in_range_defaults(hass, mqtt_mock): """Test find in range with default range.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1612,6 +1617,7 @@ async def test_find_in_range_defaults(hass, mqtt_mock): async def test_find_in_range_altered(hass, mqtt_mock): """Test find in range with altered range.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1655,6 +1661,7 @@ async def test_find_in_range_altered(hass, mqtt_mock): async def test_find_in_range_defaults_inverted(hass, mqtt_mock): """Test find in range with default range but inverted.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", @@ -1698,6 +1705,7 @@ async def test_find_in_range_defaults_inverted(hass, mqtt_mock): async def test_find_in_range_altered_inverted(hass, mqtt_mock): """Test find in range with altered range and inverted.""" mqtt_cover = MqttCover( + hass, { "name": "cover.test", "state_topic": "state-topic", diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index da4d90ad5ec..5481b8b2565 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -153,6 +153,7 @@ light: payload_off: "off" """ +import json from os import path import pytest @@ -1466,20 +1467,249 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog): assert state.name == "Beer" -async def test_discovery_update_light(hass, mqtt_mock, caplog): +async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog): """Test update of discovered light.""" - data1 = ( - '{ "name": "Beer",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic",' - ' "state_value_template": "{{value_json.power1}}" }' + data1 = json.dumps( + { + "name": "Beer", + "state_topic": "test_light_rgb/state1", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state1", + "rgb_command_topic": "test_light_rgb/rgb/set", + "color_temp_command_topic": "test_light_rgb/state1", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "white_value_command_topic": "test_light_rgb/white_value/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/state1", + "color_temp_state_topic": "test_light_rgb/state1", + "effect_state_topic": "test_light_rgb/state1", + "hs_state_topic": "test_light_rgb/state1", + "rgb_state_topic": "test_light_rgb/state1", + "white_value_state_topic": "test_light_rgb/state1", + "xy_state_topic": "test_light_rgb/state1", + "state_value_template": "{{ value_json.state1.state }}", + "brightness_value_template": "{{ value_json.state1.brightness }}", + "color_temp_value_template": "{{ value_json.state1.ct }}", + "effect_value_template": "{{ value_json.state1.fx }}", + "hs_value_template": "{{ value_json.state1.hs }}", + "rgb_value_template": "{{ value_json.state1.rgb }}", + "white_value_template": "{{ value_json.state1.white }}", + "xy_value_template": "{{ value_json.state1.xy }}", + } ) - data2 = ( - '{ "name": "Milk",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic",' - ' "state_value_template": "{{value_json.power2}}" }' + + data2 = json.dumps( + { + "name": "Milk", + "state_topic": "test_light_rgb/state2", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state2", + "rgb_command_topic": "test_light_rgb/rgb/set", + "color_temp_command_topic": "test_light_rgb/state2", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "white_value_command_topic": "test_light_rgb/white_value/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/state2", + "color_temp_state_topic": "test_light_rgb/state2", + "effect_state_topic": "test_light_rgb/state2", + "hs_state_topic": "test_light_rgb/state2", + "rgb_state_topic": "test_light_rgb/state2", + "white_value_state_topic": "test_light_rgb/state2", + "xy_state_topic": "test_light_rgb/state2", + "state_value_template": "{{ value_json.state2.state }}", + "brightness_value_template": "{{ value_json.state2.brightness }}", + "color_temp_value_template": "{{ value_json.state2.ct }}", + "effect_value_template": "{{ value_json.state2.fx }}", + "hs_value_template": "{{ value_json.state2.hs }}", + "rgb_value_template": "{{ value_json.state2.rgb }}", + "white_value_template": "{{ value_json.state2.white }}", + "xy_value_template": "{{ value_json.state2.xy }}", + } ) + state_data1 = [ + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ) + ], + "on", + [("brightness", 100), ("color_temp", 123), ("effect", "cycle")], + ), + ( + [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], + "off", + None, + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ) + ], + "on", + [("hs_color", (1, 2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"rgb":"255,127,63"}}', + ) + ], + "on", + [("rgb_color", (255, 127, 63))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ) + ], + "on", + [("white_value", 50), ("xy_color", (0.3, 0.401))], + ), + ] + state_data2 = [ + ( + [ + ( + "test_light_rgb/state2", + '{"state2":{"state":"ON", "brightness":50, "ct":200, "fx":"loop"}}', + ) + ], + "on", + [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ), + ( + "test_light_rgb/state1", + '{"state2":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ), + ( + "test_light_rgb/state2", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ), + ], + "on", + [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + ), + ( + [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], + "on", + None, + ), + ( + [("test_light_rgb/state1", '{"state2":{"state":"OFF"}}')], + "on", + None, + ), + ( + [("test_light_rgb/state2", '{"state1":{"state":"OFF"}}')], + "on", + None, + ), + ( + [("test_light_rgb/state2", '{"state2":{"state":"OFF"}}')], + "off", + None, + ), + ( + [ + ( + "test_light_rgb/state2", + '{"state2":{"state":"ON", "hs":"1.2,2.2"}}', + ) + ], + "on", + [("hs_color", (1.2, 2.2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ), + ( + "test_light_rgb/state1", + '{"state2":{"state":"ON", "hs":"1,2"}}', + ), + ( + "test_light_rgb/state2", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ), + ], + "on", + [("hs_color", (1.2, 2.2))], + ), + ( + [ + ( + "test_light_rgb/state2", + '{"state2":{"rgb":"63,127,255"}}', + ) + ], + "on", + [("rgb_color", (63, 127, 255))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"rgb":"255,127,63"}}', + ), + ( + "test_light_rgb/state1", + '{"state2":{"rgb":"255,127,63"}}', + ), + ( + "test_light_rgb/state2", + '{"state1":{"rgb":"255,127,63"}}', + ), + ], + "on", + [("rgb_color", (63, 127, 255))], + ), + ( + [ + ( + "test_light_rgb/state2", + '{"state2":{"white":75, "xy":"0.4, 0.3"}}', + ) + ], + "on", + [("white_value", 75), ("xy_color", (0.4, 0.3))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ), + ( + "test_light_rgb/state1", + '{"state2":{"white":50, "xy":"0.3, 0.4"}}', + ), + ( + "test_light_rgb/state2", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ), + ], + "on", + [("white_value", 75), ("xy_color", (0.4, 0.3))], + ), + ] + await help_test_discovery_update( hass, mqtt_mock, @@ -1487,10 +1717,221 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): light.DOMAIN, data1, data2, - state_data1=[("test_topic", '{"power1":"ON"}')], - state1="on", - state_data2=[("test_topic", '{"power2":"OFF"}')], - state2="off", + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_light_template(hass, mqtt_mock, caplog): + """Test update of discovered light.""" + data1 = json.dumps( + { + "name": "Beer", + "state_topic": "test_light_rgb/state1", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state1", + "rgb_command_topic": "test_light_rgb/rgb/set", + "color_temp_command_topic": "test_light_rgb/state1", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "white_value_command_topic": "test_light_rgb/white_value/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/state1", + "color_temp_state_topic": "test_light_rgb/state1", + "effect_state_topic": "test_light_rgb/state1", + "hs_state_topic": "test_light_rgb/state1", + "rgb_state_topic": "test_light_rgb/state1", + "white_value_state_topic": "test_light_rgb/state1", + "xy_state_topic": "test_light_rgb/state1", + "state_value_template": "{{ value_json.state1.state }}", + "brightness_value_template": "{{ value_json.state1.brightness }}", + "color_temp_value_template": "{{ value_json.state1.ct }}", + "effect_value_template": "{{ value_json.state1.fx }}", + "hs_value_template": "{{ value_json.state1.hs }}", + "rgb_value_template": "{{ value_json.state1.rgb }}", + "white_value_template": "{{ value_json.state1.white }}", + "xy_value_template": "{{ value_json.state1.xy }}", + } + ) + + data2 = json.dumps( + { + "name": "Milk", + "state_topic": "test_light_rgb/state1", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state1", + "rgb_command_topic": "test_light_rgb/rgb/set", + "color_temp_command_topic": "test_light_rgb/state1", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "white_value_command_topic": "test_light_rgb/white_value/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/state1", + "color_temp_state_topic": "test_light_rgb/state1", + "effect_state_topic": "test_light_rgb/state1", + "hs_state_topic": "test_light_rgb/state1", + "rgb_state_topic": "test_light_rgb/state1", + "white_value_state_topic": "test_light_rgb/state1", + "xy_state_topic": "test_light_rgb/state1", + "state_value_template": "{{ value_json.state2.state }}", + "brightness_value_template": "{{ value_json.state2.brightness }}", + "color_temp_value_template": "{{ value_json.state2.ct }}", + "effect_value_template": "{{ value_json.state2.fx }}", + "hs_value_template": "{{ value_json.state2.hs }}", + "rgb_value_template": "{{ value_json.state2.rgb }}", + "white_value_template": "{{ value_json.state2.white }}", + "xy_value_template": "{{ value_json.state2.xy }}", + } + ) + state_data1 = [ + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ) + ], + "on", + [("brightness", 100), ("color_temp", 123), ("effect", "cycle")], + ), + ( + [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], + "off", + None, + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ) + ], + "on", + [("hs_color", (1, 2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"rgb":"255,127,63"}}', + ) + ], + "on", + [("rgb_color", (255, 127, 63))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ) + ], + "on", + [("white_value", 50), ("xy_color", (0.3, 0.401))], + ), + ] + state_data2 = [ + ( + [ + ( + "test_light_rgb/state1", + '{"state2":{"state":"ON", "brightness":50, "ct":200, "fx":"loop"}}', + ) + ], + "on", + [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "brightness":100, "ct":123, "fx":"cycle"}}', + ), + ], + "on", + [("brightness", 50), ("color_temp", 200), ("effect", "loop")], + ), + ( + [("test_light_rgb/state1", '{"state1":{"state":"OFF"}}')], + "on", + None, + ), + ( + [("test_light_rgb/state1", '{"state2":{"state":"OFF"}}')], + "off", + None, + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state2":{"state":"ON", "hs":"1.2,2.2"}}', + ) + ], + "on", + [("hs_color", (1.2, 2.2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"state":"ON", "hs":"1,2"}}', + ) + ], + "on", + [("hs_color", (1.2, 2.2))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state2":{"rgb":"63,127,255"}}', + ) + ], + "on", + [("rgb_color", (63, 127, 255))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"rgb":"255,127,63"}}', + ) + ], + "on", + [("rgb_color", (63, 127, 255))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state2":{"white":75, "xy":"0.4, 0.3"}}', + ) + ], + "on", + [("white_value", 75), ("xy_color", (0.4, 0.3))], + ), + ( + [ + ( + "test_light_rgb/state1", + '{"state1":{"white":50, "xy":"0.3, 0.4"}}', + ) + ], + "on", + [("white_value", 75), ("xy_color", (0.4, 0.3))], + ), + ] + + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + light.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 0d31b9f33f2..77fd8c561b2 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1,4 +1,5 @@ """The tests for the MQTT sensor platform.""" +import copy from datetime import datetime, timedelta import json @@ -430,12 +431,71 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): await help_test_discovery_removal(hass, mqtt_mock, caplog, sensor.DOMAIN, data) -async def test_discovery_update_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): """Test update of discovered sensor.""" - data1 = '{ "name": "Beer", "state_topic": "test_topic" }' - data2 = '{ "name": "Milk", "state_topic": "test_topic" }' + config = {"name": "test", "state_topic": "test_topic"} + config1 = copy.deepcopy(config) + config2 = copy.deepcopy(config) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "sensor/state1" + config2["state_topic"] = "sensor/state2" + config1["value_template"] = "{{ value_json.state | int }}" + config2["value_template"] = "{{ value_json.state | int * 2 }}" + + state_data1 = [ + ([("sensor/state1", '{"state":100}')], "100", None), + ] + state_data2 = [ + ([("sensor/state1", '{"state":1000}')], "100", None), + ([("sensor/state1", '{"state":1000}')], "100", None), + ([("sensor/state2", '{"state":100}')], "200", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) await help_test_discovery_update( - hass, mqtt_mock, caplog, sensor.DOMAIN, data1, data2 + hass, + mqtt_mock, + caplog, + sensor.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): + """Test update of discovered sensor.""" + config = {"name": "test", "state_topic": "test_topic"} + config1 = copy.deepcopy(config) + config2 = copy.deepcopy(config) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "sensor/state1" + config2["state_topic"] = "sensor/state1" + config1["value_template"] = "{{ value_json.state | int }}" + config2["value_template"] = "{{ value_json.state | int * 2 }}" + + state_data1 = [ + ([("sensor/state1", '{"state":100}')], "100", None), + ] + state_data2 = [ + ([("sensor/state1", '{"state":100}')], "200", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + sensor.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index a6edb8d6f14..4d9c6dc2c77 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -1,4 +1,7 @@ """The tests for the MQTT switch platform.""" +import copy +import json + import pytest from homeassistant.components import switch @@ -304,20 +307,75 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog): await help_test_discovery_removal(hass, mqtt_mock, caplog, switch.DOMAIN, data) -async def test_discovery_update_switch(hass, mqtt_mock, caplog): +async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): """Test update of discovered switch.""" - data1 = ( - '{ "name": "Beer",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) - data2 = ( - '{ "name": "Milk",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) + config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "switch/state1" + config2["state_topic"] = "switch/state2" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("switch/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("switch/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ([("switch/state2", '{"state2":{"state":"ON"}}')], "on", None), + ([("switch/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("switch/state1", '{"state2":{"state":"OFF"}}')], "on", None), + ([("switch/state2", '{"state1":{"state":"OFF"}}')], "on", None), + ([("switch/state2", '{"state2":{"state":"OFF"}}')], "off", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) await help_test_discovery_update( - hass, mqtt_mock, caplog, switch.DOMAIN, data1, data2 + hass, + mqtt_mock, + caplog, + switch.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, + ) + + +async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): + """Test update of discovered switch.""" + config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) + config1["name"] = "Beer" + config2["name"] = "Milk" + config1["state_topic"] = "switch/state1" + config2["state_topic"] = "switch/state1" + config1["value_template"] = "{{ value_json.state1.state }}" + config2["value_template"] = "{{ value_json.state2.state }}" + + state_data1 = [ + ([("switch/state1", '{"state1":{"state":"ON"}}')], "on", None), + ] + state_data2 = [ + ([("switch/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ([("switch/state1", '{"state2":{"state":"ON"}}')], "on", None), + ([("switch/state1", '{"state1":{"state":"OFF"}}')], "on", None), + ([("switch/state1", '{"state2":{"state":"OFF"}}')], "off", None), + ] + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + await help_test_discovery_update( + hass, + mqtt_mock, + caplog, + switch.DOMAIN, + data1, + data2, + state_data1=state_data1, + state_data2=state_data2, ) From 589086f0d07040227db9048911d6f07699fe7cc6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 18:53:37 +0000 Subject: [PATCH 786/862] Bumped version to 0.115.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 465f93c3f76..7a32ea1ee58 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 1720b71d62fba200b70fa19165aef7796b370bd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 05:19:21 -0500 Subject: [PATCH 787/862] Limit zeroconf discovery to name/macaddress when provided (#39877) Co-authored-by: Paulus Schoutsen --- homeassistant/components/axis/manifest.json | 6 +- .../components/brother/manifest.json | 2 +- .../components/doorbird/manifest.json | 2 +- homeassistant/components/shelly/manifest.json | 2 +- .../components/smappee/manifest.json | 3 +- homeassistant/components/zeroconf/__init__.py | 21 +++- homeassistant/generated/zeroconf.py | 113 +++++++++++++---- homeassistant/loader.py | 19 ++- script/hassfest/manifest.py | 13 +- script/hassfest/zeroconf.py | 13 +- tests/components/zeroconf/test_init.py | 118 ++++++++++++++++-- tests/test_loader.py | 46 ++++++- 12 files changed, 303 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 3b08c5ad4d4..ceb926f326e 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -4,7 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==35"], - "zeroconf": ["_axis-video._tcp.local."], + "zeroconf": [ + {"type":"_axis-video._tcp.local.","macaddress":"00408C*"}, + {"type":"_axis-video._tcp.local.","macaddress":"ACCC8E*"}, + {"type":"_axis-video._tcp.local.","macaddress":"B8A44F*"} + ], "after_dependencies": ["mqtt"], "codeowners": ["@Kane610"] } diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index f107c9573da..2e73f9b8450 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], "requirements": ["brother==0.1.17"], - "zeroconf": ["_printer._tcp.local."], + "zeroconf": [{"type": "_printer._tcp.local.", "name":"brother*"}], "config_flow": true, "quality_scale": "platinum" } diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 23495a22bf8..c5805b15eac 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": ["doorbirdpy==2.1.0"], "dependencies": ["http"], - "zeroconf": ["_axis-video._tcp.local."], + "zeroconf": [{"type":"_axis-video._tcp.local.","macaddress":"1CCAE3*"}], "codeowners": ["@oblogic7", "@bdraco"], "config_flow": true } diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 357f2c10fda..1c12125fd89 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", "requirements": ["aioshelly==0.3.0"], - "zeroconf": ["_http._tcp.local."], + "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu"] } diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index c6cac118b72..ba1005b87d4 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -11,6 +11,7 @@ "@bsmappee" ], "zeroconf": [ - "_ssh._tcp.local." + {"type":"_ssh._tcp.local.", "name":"smappee1*"}, + {"type":"_ssh._tcp.local.", "name":"smappee2*"} ] } diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index f570a30baa6..51da3638a9e 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,5 +1,6 @@ """Support for exposing Home Assistant via Zeroconf.""" import asyncio +import fnmatch import ipaddress import logging import socket @@ -268,10 +269,26 @@ def setup(hass, config): # likely bad homekit data return - for domain in zeroconf_types[service_type]: + for entry in zeroconf_types[service_type]: + if len(entry) > 1: + if "macaddress" in entry: + if "properties" not in info: + continue + if "macaddress" not in info["properties"]: + continue + if not fnmatch.fnmatch( + info["properties"]["macaddress"], entry["macaddress"] + ): + continue + if "name" in entry: + if "name" not in info: + continue + if not fnmatch.fnmatch(info["name"], entry["name"]): + continue + hass.add_job( hass.config_entries.flow.async_init( - domain, context={"source": DOMAIN}, data=info + entry["domain"], context={"source": DOMAIN}, data=info ) ) diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index ea61ccfbaeb..ba49666ded3 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -7,72 +7,137 @@ To update, run python3 -m script.hassfest ZEROCONF = { "_Volumio._tcp.local.": [ - "volumio" + { + "domain": "volumio" + } ], "_api._udp.local.": [ - "guardian" + { + "domain": "guardian" + } ], "_axis-video._tcp.local.": [ - "axis", - "doorbird" + { + "domain": "axis", + "macaddress": "00408C*" + }, + { + "domain": "axis", + "macaddress": "ACCC8E*" + }, + { + "domain": "axis", + "macaddress": "B8A44F*" + }, + { + "domain": "doorbird", + "macaddress": "1CCAE3*" + } ], "_bond._tcp.local.": [ - "bond" + { + "domain": "bond" + } ], "_daap._tcp.local.": [ - "forked_daapd" + { + "domain": "forked_daapd" + } ], "_dkapi._tcp.local.": [ - "daikin" + { + "domain": "daikin" + } ], "_elg._tcp.local.": [ - "elgato" + { + "domain": "elgato" + } ], "_esphomelib._tcp.local.": [ - "esphome" + { + "domain": "esphome" + } ], "_googlecast._tcp.local.": [ - "cast" + { + "domain": "cast" + } ], "_hap._tcp.local.": [ - "homekit_controller" + { + "domain": "homekit_controller" + } ], "_http._tcp.local.": [ - "shelly" + { + "domain": "shelly", + "name": "shelly*" + } ], "_ipp._tcp.local.": [ - "ipp" + { + "domain": "ipp" + } ], "_ipps._tcp.local.": [ - "ipp" + { + "domain": "ipp" + } ], "_miio._udp.local.": [ - "xiaomi_aqara", - "xiaomi_miio" + { + "domain": "xiaomi_aqara" + }, + { + "domain": "xiaomi_miio" + } ], "_nut._tcp.local.": [ - "nut" + { + "domain": "nut" + } ], "_plugwise._tcp.local.": [ - "plugwise" + { + "domain": "plugwise" + } ], "_printer._tcp.local.": [ - "brother" + { + "domain": "brother", + "name": "brother*" + } ], "_spotify-connect._tcp.local.": [ - "spotify" + { + "domain": "spotify" + } ], "_ssh._tcp.local.": [ - "smappee" + { + "domain": "smappee", + "name": "smappee1*" + }, + { + "domain": "smappee", + "name": "smappee2*" + } ], "_viziocast._tcp.local.": [ - "vizio" + { + "domain": "vizio" + } ], "_wled._tcp.local.": [ - "wled" + { + "domain": "wled" + } ], "_xbmc-jsonrpc-h._tcp.local.": [ - "kodi" + { + "domain": "kodi" + } ] } diff --git a/homeassistant/loader.py b/homeassistant/loader.py index c5027710c47..53f793678c0 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -145,18 +145,25 @@ async def async_get_config_flows(hass: "HomeAssistant") -> Set[str]: return flows -async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List]: +async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str]]]: """Return cached list of zeroconf types.""" - zeroconf: Dict[str, List] = ZEROCONF.copy() + zeroconf: Dict[str, List[Dict[str, str]]] = ZEROCONF.copy() integrations = await async_get_custom_components(hass) for integration in integrations.values(): if not integration.zeroconf: continue - for typ in integration.zeroconf: - zeroconf.setdefault(typ, []) - if integration.domain not in zeroconf[typ]: - zeroconf[typ].append(integration.domain) + for entry in integration.zeroconf: + data = {"domain": integration.domain} + if isinstance(entry, dict): + typ = entry["type"] + entry_without_type = entry.copy() + del entry_without_type["type"] + data.update(entry_without_type) + else: + typ = entry + + zeroconf.setdefault(typ, []).append(data) return zeroconf diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index cd3895f5f20..b0148b0911a 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -38,7 +38,18 @@ MANIFEST_SCHEMA = vol.Schema( vol.Required("domain"): str, vol.Required("name"): str, vol.Optional("config_flow"): bool, - vol.Optional("zeroconf"): [str], + vol.Optional("zeroconf"): [ + vol.Any( + str, + vol.Schema( + { + vol.Required("type"): str, + vol.Optional("macaddress"): str, + vol.Optional("name"): str, + } + ), + ) + ], vol.Optional("ssdp"): vol.Schema( vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index d6b39bd0d27..61162b02761 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -37,8 +37,17 @@ def generate_and_validate(integrations: Dict[str, Integration]): if not (service_types or homekit_models): continue - for service_type in service_types: - service_type_dict[service_type].append(domain) + for entry in service_types: + data = {"domain": domain} + if isinstance(entry, dict): + typ = entry["type"] + entry_without_type = entry.copy() + del entry_without_type["type"] + data.update(entry_without_type) + else: + typ = entry + + service_type_dict[typ].append(data) for model in homekit_models: if model in homekit_dict: diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 73629596c35..ae1f6d5fd98 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -79,6 +79,24 @@ def get_homekit_info_mock(model, pairing_status): return mock_homekit_info +def get_zeroconf_info_mock(macaddress): + """Return info for get_service_info for an zeroconf device.""" + + def mock_zc_info(service_type, name): + return ServiceInfo( + service_type, + name, + addresses=[b"\n\x00\x00\x14"], + port=80, + weight=0, + priority=0, + server="name.local.", + properties={b"macaddress": macaddress.encode()}, + ) + + return mock_zc_info + + async def test_setup(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.object( @@ -94,7 +112,11 @@ async def test_setup(hass, mock_zeroconf): assert len(mock_service_browser.mock_calls) == 1 expected_flow_calls = 0 for matching_components in zc_gen.ZEROCONF.values(): - expected_flow_calls += len(matching_components) + domains = set() + for component in matching_components: + if len(component) == 1: + domains.add(component["domain"]) + expected_flow_calls += len(domains) assert len(mock_config_flow.mock_calls) == expected_flow_calls # Test instance is set. @@ -209,10 +231,77 @@ async def test_service_with_invalid_name(hass, mock_zeroconf, caplog): assert "Failed to get info for device name" in caplog.text +async def test_zeroconf_match(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + + def http_only_service_update_mock(zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_http._tcp.local.", + "shelly108._http._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( + "FFAADDCC11DD" + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "shelly" + + +async def test_zeroconf_no_match(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + + def http_only_service_update_mock(zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_http._tcp.local.", + "somethingelse._http._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( + "FFAADDCC11DD" + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 0 + + async def test_homekit_match_partial_space(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -233,7 +322,9 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): async def test_homekit_match_partial_dash(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -254,7 +345,9 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): async def test_homekit_match_full(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -267,11 +360,6 @@ async def test_homekit_match_full(hass, mock_zeroconf): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - homekit_mock = get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED) - info = homekit_mock("_hap._tcp.local.", "BSB002._hap._tcp.local.") - import pprint - - pprint.pprint(["homekit", info]) assert len(mock_service_browser.mock_calls) == 1 assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "hue" @@ -280,7 +368,9 @@ async def test_homekit_match_full(hass, mock_zeroconf): async def test_homekit_already_paired(hass, mock_zeroconf): """Test that an already paired device is sent to homekit_controller.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -302,7 +392,9 @@ async def test_homekit_already_paired(hass, mock_zeroconf): async def test_homekit_invalid_paring_status(hass, mock_zeroconf): """Test that missing paring data is not sent to homekit_controller.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( @@ -323,7 +415,9 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf): async def test_homekit_not_paired(hass, mock_zeroconf): """Test that an not paired device is sent to homekit_controller.""" with patch.dict( - zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + zc_gen.ZEROCONF, + {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( diff --git a/tests/test_loader.py b/tests/test_loader.py index 272b0453469..f5ba54ff269 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -218,6 +218,23 @@ def test_integration_properties(hass): assert integration.zeroconf is None assert integration.ssdp is None + integration = loader.Integration( + hass, + "custom_components.hue", + None, + { + "name": "Philips Hue", + "domain": "hue", + "dependencies": ["test-dep"], + "zeroconf": [{"type": "_hue._tcp.local.", "name": "hue*"}], + "requirements": ["test-req==1.0.0"], + }, + ) + assert integration.is_built_in is False + assert integration.homekit is None + assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}] + assert integration.ssdp is None + async def test_integrations_only_once(hass): """Test that we load integrations only once.""" @@ -253,6 +270,25 @@ def _get_test_integration(hass, name, config_flow): ) +def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow): + """Return a generated test integration with a zeroconf matcher.""" + return loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": config_flow, + "dependencies": [], + "requirements": [], + "zeroconf": [{"type": f"_{name}._tcp.local.", "name": f"{name}*"}], + "homekit": {"models": [name]}, + "ssdp": [{"manufacturer": name, "modelName": name}], + }, + ) + + async def test_get_custom_components(hass): """Verify that custom components are cached.""" test_1_integration = _get_test_integration(hass, "test_1", False) @@ -289,7 +325,9 @@ async def test_get_config_flows(hass): async def test_get_zeroconf(hass): """Verify that custom components with zeroconf are found.""" test_1_integration = _get_test_integration(hass, "test_1", True) - test_2_integration = _get_test_integration(hass, "test_2", True) + test_2_integration = _get_test_integration_with_zeroconf_matcher( + hass, "test_2", True + ) with patch("homeassistant.loader.async_get_custom_components") as mock_get: mock_get.return_value = { @@ -297,8 +335,10 @@ async def test_get_zeroconf(hass): "test_2": test_2_integration, } zeroconf = await loader.async_get_zeroconf(hass) - assert zeroconf["_test_1._tcp.local."] == ["test_1"] - assert zeroconf["_test_2._tcp.local."] == ["test_2"] + assert zeroconf["_test_1._tcp.local."] == [{"domain": "test_1"}] + assert zeroconf["_test_2._tcp.local."] == [ + {"domain": "test_2", "name": "test_2*"} + ] async def test_get_homekit(hass): From 3d4913348a00b35aa8ba0bcdee3b1e268ea07e29 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Fri, 11 Sep 2020 13:09:31 +0200 Subject: [PATCH 788/862] Warn users if KNX has no devices configured (#39899) Co-authored-by: Martin Hjelmare --- homeassistant/components/knx/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 7e56d2a955a..5a2f29e6247 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -148,6 +148,12 @@ async def async_setup(hass, config): discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config) ) + if not hass.data[DATA_KNX].xknx.devices: + _LOGGER.warning( + "No KNX devices are configured. Please read " + "https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes" + ) + hass.services.async_register( DOMAIN, SERVICE_KNX_SEND, From 7eade4029ab3cde6b3f6b06b6a16c16cf51af032 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 11 Sep 2020 13:08:13 +0200 Subject: [PATCH 789/862] Add children media class (#39902) Co-authored-by: Paulus Schoutsen --- homeassistant/components/kodi/browse_media.py | 39 +++++++++++----- .../components/media_player/__init__.py | 38 +++++++++------- .../components/media_source/models.py | 4 +- .../components/netatmo/media_source.py | 5 ++- .../components/philips_js/media_player.py | 3 +- .../components/plex/media_browser.py | 40 ++++++++++++++--- homeassistant/components/roku/browse_media.py | 18 ++++++-- .../components/sonos/media_player.py | 40 ++++++++++++++--- .../components/spotify/media_player.py | 45 +++++++++---------- tests/components/media_source/test_models.py | 3 ++ tests/components/plex/mock_classes.py | 5 +++ tests/components/roku/test_media_player.py | 8 ++++ 12 files changed, 181 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 3fc3b40cd38..c7df170b5c9 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -29,8 +29,15 @@ PLAYABLE_MEDIA_TYPES = [ MEDIA_TYPE_TRACK, ] -CONTENT_TYPE_MEDIA_CLASS = { - "library_music": MEDIA_CLASS_MUSIC, +CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = { + MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, + MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, + MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, + MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, + MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, +} + +CHILD_TYPE_MEDIA_CLASS = { MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, @@ -151,8 +158,10 @@ async def build_item_response(media_library, payload): except UnknownMediaType: pass - return BrowseMedia( - media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], + response = BrowseMedia( + media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get( + search_type, MEDIA_CLASS_DIRECTORY + ), media_content_id=search_id, media_content_type=search_type, title=title, @@ -162,6 +171,13 @@ async def build_item_response(media_library, payload): thumbnail=thumbnail, ) + if search_type == "library_music": + response.children_media_class = MEDIA_CLASS_MUSIC + else: + response.calculate_children_class() + + return response + def item_payload(item, media_library): """ @@ -170,11 +186,12 @@ def item_payload(item, media_library): Used by async_browse_media. """ title = item["label"] - thumbnail = item.get("thumbnail") if thumbnail: thumbnail = media_library.thumbnail_url(thumbnail) + media_class = None + if "songid" in item: media_content_type = MEDIA_TYPE_TRACK media_content_id = f"{item['songid']}" @@ -213,16 +230,18 @@ def item_payload(item, media_library): else: # this case is for the top folder of each type # possible content types: album, artist, movie, library_music, tvshow + media_class = MEDIA_CLASS_DIRECTORY media_content_type = item["type"] media_content_id = "" can_play = False can_expand = True - try: - media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] - except KeyError as err: - _LOGGER.debug("Unknown media type received: %s", media_content_type) - raise UnknownMediaType from err + if media_class is None: + try: + media_class = CHILD_TYPE_MEDIA_CLASS[media_content_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received: %s", media_content_type) + raise UnknownMediaType from err return BrowseMedia( title=title, diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 718011b4a76..348bc521a5a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -85,6 +85,7 @@ from .const import ( ATTR_SOUND_MODE, ATTR_SOUND_MODE_LIST, DOMAIN, + MEDIA_CLASS_DIRECTORY, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOUND_MODE, @@ -816,24 +817,10 @@ class MediaPlayerEntity(Entity): media_content_type: Optional[str] = None, media_content_id: Optional[str] = None, ) -> "BrowseMedia": - """ - Return a payload for the "media_player/browse_media" websocket command. + """Return a BrowseMedia instance. - Payload should follow this format: - { - "title": str - Title of the item - "media_class": str - Media class - "media_content_type": str - see below - "media_content_id": str - see below - - Can be passed back in to browse further - - Can be used as-is with media_player.play_media service - "can_play": bool - If item is playable - "can_expand": bool - If item contains other media - "thumbnail": str (Optional) - URL to image thumbnail for item - "children": list (Optional) - [{}, ...] - } - - Note: Children should omit the children key. + The BrowseMedia instance will be used by the + "media_player/browse_media" websocket command. """ raise NotImplementedError() @@ -1054,6 +1041,7 @@ class BrowseMedia: can_play: bool, can_expand: bool, children: Optional[List["BrowseMedia"]] = None, + children_media_class: Optional[str] = None, thumbnail: Optional[str] = None, ): """Initialize browse media item.""" @@ -1064,10 +1052,14 @@ class BrowseMedia: self.can_play = can_play self.can_expand = can_expand self.children = children + self.children_media_class = children_media_class self.thumbnail = thumbnail def as_dict(self, *, parent: bool = True) -> dict: """Convert Media class to browse media dictionary.""" + if self.children_media_class is None: + self.calculate_children_class() + response = { "title": self.title, "media_class": self.media_class, @@ -1075,6 +1067,7 @@ class BrowseMedia: "media_content_id": self.media_content_id, "can_play": self.can_play, "can_expand": self.can_expand, + "children_media_class": self.children_media_class, "thumbnail": self.thumbnail, } @@ -1089,3 +1082,14 @@ class BrowseMedia: response["children"] = [] return response + + def calculate_children_class(self) -> None: + """Count the children media classes and calculate the correct class.""" + if self.children is None or len(self.children) == 0: + return + + self.children_media_class = MEDIA_CLASS_DIRECTORY + + proposed_class = self.children[0].media_class + if all(child.media_class == proposed_class for child in self.children): + self.children_media_class = proposed_class diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 3b2044d7e0f..e16ecbe578e 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -6,6 +6,7 @@ from typing import List, Optional, Tuple from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, ) @@ -53,11 +54,12 @@ class MediaSourceItem: base = BrowseMediaSource( domain=None, identifier=None, - media_class=MEDIA_CLASS_CHANNEL, + media_class=MEDIA_CLASS_DIRECTORY, media_content_type=MEDIA_TYPE_CHANNELS, title="Media Sources", can_play=False, can_expand=True, + children_media_class=MEDIA_CLASS_CHANNEL, ) base.children = [ BrowseMediaSource( diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 02ffd608472..76527677224 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -5,6 +5,7 @@ import re from typing import Optional, Tuple from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_VIDEO, MEDIA_TYPE_VIDEO, ) @@ -91,10 +92,12 @@ class NetatmoSource(MediaSource): else: path = f"{source}/{camera_id}" + media_class = MEDIA_CLASS_DIRECTORY if event_id is None else MEDIA_CLASS_VIDEO + media = BrowseMediaSource( domain=DOMAIN, identifier=path, - media_class=MEDIA_CLASS_VIDEO, + media_class=media_class, media_content_type=MEDIA_TYPE_VIDEO, title=title, can_play=bool( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 7da485fca74..7e2e126437c 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_CHANNELS, SUPPORT_BROWSE_MEDIA, @@ -289,7 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): return BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_CHANNEL, + media_class=MEDIA_CLASS_DIRECTORY, media_content_id="", media_content_type=MEDIA_TYPE_CHANNELS, can_play=False, diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 9d5572a4faa..28444c4a351 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -26,7 +26,7 @@ class UnknownMediaType(BrowseError): EXPANDABLES = ["album", "artist", "playlist", "season", "show"] PLAYLISTS_BROWSE_PAYLOAD = { "title": "Playlists", - "media_class": MEDIA_CLASS_PLAYLIST, + "media_class": MEDIA_CLASS_DIRECTORY, "media_content_id": "all", "media_content_type": "playlists", "can_play": False, @@ -94,10 +94,21 @@ def browse_media( if special_folder: if media_content_type == "server": library_or_section = plex_server.library + children_media_class = MEDIA_CLASS_DIRECTORY title = plex_server.friendly_name elif media_content_type == "library": library_or_section = plex_server.library.sectionByID(media_content_id) title = library_or_section.title + try: + children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE] + except KeyError as err: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) from err + else: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) payload = { "title": title, @@ -107,6 +118,7 @@ def browse_media( "can_play": False, "can_expand": True, "children": [], + "children_media_class": children_media_class, } method = SPECIAL_METHODS[special_folder] @@ -116,13 +128,20 @@ def browse_media( payload["children"].append(item_payload(item)) except UnknownMediaType: continue + return BrowseMedia(**payload) - if media_content_type in ["server", None]: - return server_payload(plex_server) + try: + if media_content_type in ["server", None]: + return server_payload(plex_server) - if media_content_type == "library": - return library_payload(plex_server, media_content_id) + if media_content_type == "library": + return library_payload(plex_server, media_content_id) + + except UnknownMediaType as err: + raise BrowseError( + f"Media not found: {media_content_type} / {media_content_id}" + ) from err if media_content_type == "playlists": return playlists_payload(plex_server) @@ -160,6 +179,11 @@ def item_payload(item): def library_section_payload(section): """Create response payload for a single library section.""" + try: + children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE] + except KeyError as err: + _LOGGER.debug("Unknown type received: %s", section.TYPE) + raise UnknownMediaType from err return BrowseMedia( title=section.title, media_class=MEDIA_CLASS_DIRECTORY, @@ -167,6 +191,7 @@ def library_section_payload(section): media_content_type="library", can_play=False, can_expand=True, + children_media_class=children_media_class, ) @@ -194,6 +219,7 @@ def server_payload(plex_server): can_expand=True, ) server_info.children = [] + server_info.children_media_class = MEDIA_CLASS_DIRECTORY server_info.children.append(special_library_payload(server_info, "On Deck")) server_info.children.append(special_library_payload(server_info, "Recently Added")) for library in plex_server.library.sections(): @@ -229,4 +255,6 @@ def playlists_payload(plex_server): playlists_info["children"].append(item_payload(playlist)) except UnknownMediaType: continue - return BrowseMedia(**playlists_info) + response = BrowseMedia(**playlists_info) + response.children_media_class = MEDIA_CLASS_PLAYLIST + return response diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index 809c6ac3578..f6f8c8976f1 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -13,9 +13,9 @@ from homeassistant.components.media_player.const import ( CONTENT_TYPE_MEDIA_CLASS = { MEDIA_TYPE_APP: MEDIA_CLASS_APP, - MEDIA_TYPE_APPS: MEDIA_CLASS_APP, + MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, - MEDIA_TYPE_CHANNELS: MEDIA_CLASS_CHANNEL, + MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY, } PLAYABLE_MEDIA_TYPES = [ @@ -59,7 +59,7 @@ def build_item_response(coordinator, payload): return None return BrowseMedia( - media_class=CONTENT_TYPE_MEDIA_CLASS[search_type], + media_class=MEDIA_CLASS_DIRECTORY, media_content_id=search_id, media_content_type=search_type, title=title, @@ -139,4 +139,16 @@ def library_payload(coordinator): ) ) + if all( + child.media_content_type == MEDIA_TYPE_APPS for child in library_info.children + ): + library_info.children_media_class = MEDIA_CLASS_APP + elif all( + child.media_content_type == MEDIA_TYPE_CHANNELS + for child in library_info.children + ): + library_info.children_media_class = MEDIA_CLASS_CHANNEL + else: + library_info.children_media_class = MEDIA_CLASS_DIRECTORY + return library_info diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 51287f9f288..2b50f2864dc 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -222,6 +222,10 @@ ATTR_STATUS_LIGHT = "status_light" UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} +class UnknownMediaType(BrowseError): + """Unknown media type.""" + + class SonosData: """Storage class for platform global data.""" @@ -1487,7 +1491,20 @@ def build_item_response(media_library, payload): except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] - media_class = SONOS_TO_MEDIA_CLASSES[MEDIA_TYPES_TO_SONOS[payload["search_type"]]] + try: + media_class = SONOS_TO_MEDIA_CLASSES[ + MEDIA_TYPES_TO_SONOS[payload["search_type"]] + ] + except KeyError: + _LOGGER.debug("Unknown media type received %s", payload["search_type"]) + return None + + children = [] + for item in media: + try: + children.append(item_payload(item)) + except UnknownMediaType: + pass return BrowseMedia( title=title, @@ -1495,7 +1512,7 @@ def build_item_response(media_library, payload): media_class=media_class, media_content_id=payload["idstring"], media_content_type=payload["search_type"], - children=[item_payload(item) for item in media], + children=children, can_play=can_play(payload["search_type"]), can_expand=can_expand(payload["search_type"]), ) @@ -1507,12 +1524,18 @@ def item_payload(item): Used by async_browse_media. """ + media_type = get_media_type(item) + try: + media_class = SONOS_TO_MEDIA_CLASSES[media_type] + except KeyError as err: + _LOGGER.debug("Unknown media type received %s", media_type) + raise UnknownMediaType from err return BrowseMedia( title=item.title, thumbnail=getattr(item, "album_art_uri", None), - media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)], + media_class=media_class, media_content_id=get_content_id(item), - media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)], + media_content_type=SONOS_TO_MEDIA_TYPES[media_type], can_play=can_play(item.item_class), can_expand=can_expand(item), ) @@ -1524,6 +1547,13 @@ def library_payload(media_library): Used by async_browse_media. """ + children = [] + for item in media_library.browse(): + try: + children.append(item_payload(item)) + except UnknownMediaType: + pass + return BrowseMedia( title="Music Library", media_class=MEDIA_CLASS_DIRECTORY, @@ -1531,7 +1561,7 @@ def library_payload(media_library): media_content_type="library", can_play=False, can_expand=True, - children=[item_payload(item) for item in media_library.browse()], + children=children, ) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index e2850027229..7beea59a7bd 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -105,18 +105,18 @@ LIBRARY_MAP = { } CONTENT_TYPE_MEDIA_CLASS = { - "current_user_playlists": MEDIA_CLASS_PLAYLIST, - "current_user_followed_artists": MEDIA_CLASS_ARTIST, - "current_user_saved_albums": MEDIA_CLASS_ALBUM, - "current_user_saved_tracks": MEDIA_CLASS_TRACK, - "current_user_saved_shows": MEDIA_CLASS_PODCAST, - "current_user_recently_played": MEDIA_CLASS_TRACK, - "current_user_top_artists": MEDIA_CLASS_ARTIST, - "current_user_top_tracks": MEDIA_CLASS_TRACK, - "featured_playlists": MEDIA_CLASS_PLAYLIST, - "categories": MEDIA_CLASS_GENRE, - "category_playlists": MEDIA_CLASS_PLAYLIST, - "new_releases": MEDIA_CLASS_ALBUM, + "current_user_playlists": MEDIA_CLASS_DIRECTORY, + "current_user_followed_artists": MEDIA_CLASS_DIRECTORY, + "current_user_saved_albums": MEDIA_CLASS_DIRECTORY, + "current_user_saved_tracks": MEDIA_CLASS_DIRECTORY, + "current_user_saved_shows": MEDIA_CLASS_DIRECTORY, + "current_user_recently_played": MEDIA_CLASS_DIRECTORY, + "current_user_top_artists": MEDIA_CLASS_DIRECTORY, + "current_user_top_tracks": MEDIA_CLASS_DIRECTORY, + "featured_playlists": MEDIA_CLASS_DIRECTORY, + "categories": MEDIA_CLASS_DIRECTORY, + "category_playlists": MEDIA_CLASS_DIRECTORY, + "new_releases": MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, @@ -567,6 +567,7 @@ def build_item_response(spotify, user, payload): can_expand=True, ) ) + media_item.children_media_class = MEDIA_CLASS_GENRE return media_item if title is None: @@ -575,7 +576,7 @@ def build_item_response(spotify, user, payload): else: title = LIBRARY_MAP.get(payload["media_content_id"]) - response = { + params = { "title": title, "media_class": media_class, "media_content_id": media_content_id, @@ -586,16 +587,16 @@ def build_item_response(spotify, user, payload): } for item in items: try: - response["children"].append(item_payload(item)) + params["children"].append(item_payload(item)) except (MissingMediaInformation, UnknownMediaType): continue if "images" in media: - response["thumbnail"] = fetch_image_url(media) + params["thumbnail"] = fetch_image_url(media) elif image: - response["thumbnail"] = image + params["thumbnail"] = image - return BrowseMedia(**response) + return BrowseMedia(**params) def item_payload(item): @@ -624,17 +625,13 @@ def item_payload(item): payload = { "title": item.get("name"), + "media_class": media_class, "media_content_id": media_id, "media_content_type": media_type, "can_play": media_type in PLAYABLE_MEDIA_TYPES, "can_expand": can_expand, } - payload = { - **payload, - "media_class": media_class, - } - if "images" in item: payload["thumbnail"] = fetch_image_url(item) elif MEDIA_TYPE_ALBUM in item: @@ -665,7 +662,9 @@ def library_payload(): {"name": item["name"], "type": item["type"], "uri": item["type"]} ) ) - return BrowseMedia(**library_info) + response = BrowseMedia(**library_info) + response.children_media_class = MEDIA_CLASS_DIRECTORY + return response def fetch_image_url(item, key="images"): diff --git a/tests/components/media_source/test_models.py b/tests/components/media_source/test_models.py index 3d19edd722d..8372382bb7a 100644 --- a/tests/components/media_source/test_models.py +++ b/tests/components/media_source/test_models.py @@ -17,6 +17,7 @@ async def test_browse_media_as_dict(): title="media/", can_play=False, can_expand=True, + children_media_class=MEDIA_CLASS_MUSIC, ) base.children = [ models.BrowseMediaSource( @@ -37,6 +38,7 @@ async def test_browse_media_as_dict(): assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media" assert not item["can_play"] assert item["can_expand"] + assert item["children_media_class"] == MEDIA_CLASS_MUSIC assert len(item["children"]) == 1 assert item["children"][0]["title"] == "test.mp3" assert item["children"][0]["media_class"] == MEDIA_CLASS_MUSIC @@ -62,6 +64,7 @@ async def test_browse_media_parent_no_children(): assert not item["can_play"] assert item["can_expand"] assert len(item["children"]) == 0 + assert item["children_media_class"] is None async def test_media_source_default_name(): diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 7cdac1b669a..e16d5cdc13b 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -409,6 +409,11 @@ class MockPlexLibrarySection: if self.title == "Photos": return "photo" + @property + def TYPE(self): + """Return the library type.""" + return self.type + @property def key(self): """Mock the key identifier property.""" diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 312770a873a..e9d5091d664 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -16,6 +16,9 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_TITLE, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MP_DOMAIN, + MEDIA_CLASS_APP, + MEDIA_CLASS_CHANNEL, + MEDIA_CLASS_DIRECTORY, MEDIA_TYPE_APP, MEDIA_TYPE_APPS, MEDIA_TYPE_CHANNEL, @@ -499,6 +502,7 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): assert msg["result"] assert msg["result"]["title"] == "Media Library" + assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY assert msg["result"]["media_content_type"] == "library" assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] @@ -523,10 +527,12 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): assert msg["result"] assert msg["result"]["title"] == "Apps" + assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 11 + assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP assert msg["result"]["children"][0]["title"] == "Satellite TV" assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP @@ -565,10 +571,12 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): assert msg["result"] assert msg["result"]["title"] == "Channels" + assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY assert msg["result"]["media_content_type"] == MEDIA_TYPE_CHANNELS assert msg["result"]["can_expand"] assert not msg["result"]["can_play"] assert len(msg["result"]["children"]) == 2 + assert msg["result"]["children_media_class"] == MEDIA_CLASS_CHANNEL assert msg["result"]["children"][0]["title"] == "WhatsOn" assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_CHANNEL From b0b957977838c7332e39e056df4b0b1e76a326b6 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 11 Sep 2020 03:55:55 +0800 Subject: [PATCH 790/862] Disable audio for HLS or mpegts input (#39906) --- homeassistant/components/stream/worker.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index d0ba30666b0..0e861c5cefc 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -64,11 +64,16 @@ def _stream_worker_internal(hass, stream, quit_event): video_stream = container.streams.video[0] except (KeyError, IndexError): _LOGGER.error("Stream has no video") + container.close() return try: audio_stream = container.streams.audio[0] except (KeyError, IndexError): audio_stream = None + # These formats need aac_adtstoasc bitstream filter, but auto_bsf not + # compatible with empty_moov and manual bitstream filters not in PyAV + if container.format.name in {"hls", "mpegts"}: + audio_stream = None # The presentation timestamps of the first packet in each stream we receive # Use to adjust before muxing or outputting, but we don't adjust internally @@ -238,7 +243,7 @@ def _stream_worker_internal(hass, stream, quit_event): # Update last_dts processed last_dts[packet.stream] = packet.dts - # mux video packets immediately, save audio packets to be muxed all at once + # mux packets if packet.stream == video_stream: mux_video_packet(packet) # mutates packet timestamps else: From b107e87d3842c49241b691d2894ecea278a35f42 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 11 Sep 2020 14:02:17 +0200 Subject: [PATCH 791/862] Don't trigger on attribute when the attribute doesn't change (#39910) Co-authored-by: Paulus Schoutsen --- .../homeassistant/triggers/state.py | 7 ++++ .../homeassistant/triggers/test_state.py | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 0fa7a98b562..f57db0ed56a 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -80,6 +80,13 @@ async def async_attach_trigger( else: new_value = to_s.attributes.get(attribute) + # When we listen for state changes with `match_all`, we + # will trigger even if just an attribute changes. When + # we listen to just an attribute, we should ignore all + # other attribute changes. + if attribute is not None and old_value == new_value: + return + if ( not match_from_state(old_value) or not match_to_state(new_value) diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 68ce907bdae..ce9ecaba1b0 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -1112,6 +1112,42 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls assert len(calls) == 1 +async def test_attribute_if_fires_on_entity_where_attr_stays_constant(hass, calls): + """Test for firing if attribute stays the same.""" + hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "state", + "entity_id": "test.entity", + "attribute": "name", + }, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + # Leave all attributes the same + hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "old_value"}) + await hass.async_block_till_done() + assert len(calls) == 0 + + # Change the untracked attribute + hass.states.async_set("test.entity", "bla", {"name": "hello", "other": "new_value"}) + await hass.async_block_till_done() + assert len(calls) == 0 + + # Change the tracked attribute + hass.states.async_set("test.entity", "bla", {"name": "world", "other": "old_value"}) + await hass.async_block_till_done() + assert len(calls) == 1 + + async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( hass, calls ): From 8ef04268be1cb2c5d2bc489d62bef79e1b51214f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Sep 2020 12:24:16 +0200 Subject: [PATCH 792/862] Extract variable rendering (#39934) --- .../components/automation/__init__.py | 21 +++---- homeassistant/components/script/__init__.py | 2 +- homeassistant/helpers/config_validation.py | 11 +++- homeassistant/helpers/script.py | 19 +++--- homeassistant/helpers/script_variables.py | 57 ++++++++++++++++++ tests/components/automation/test_init.py | 21 ++++++- tests/components/script/test_init.py | 27 ++++++++- tests/helpers/test_script_variables.py | 60 +++++++++++++++++++ 8 files changed, 193 insertions(+), 25 deletions(-) create mode 100644 homeassistant/helpers/script_variables.py create mode 100644 tests/helpers/test_script_variables.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 392ca710000..dff751956a7 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -45,6 +45,7 @@ from homeassistant.helpers.script import ( Script, make_script_schema, ) +from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.helpers.typing import TemplateVarsType @@ -256,8 +257,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._referenced_entities: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None self._logger = _LOGGER - self._variables = variables - self._variables_dynamic = template.is_complex(variables) + self._variables: ScriptVariables = variables @property def name(self): @@ -334,9 +334,6 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Startup with initial state or previous state.""" await super().async_added_to_hass() - if self._variables_dynamic: - template.attach(cast(HomeAssistant, self.hass), self._variables) - self._logger = logging.getLogger( f"{__name__}.{split_entity_id(self.entity_id)[1]}" ) @@ -392,15 +389,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity): This method is a coroutine. """ if self._variables: - if self._variables_dynamic: - variables = template.render_complex(self._variables, run_variables) - else: - variables = dict(self._variables) + try: + variables = self._variables.async_render(self.hass, run_variables) + except template.TemplateError as err: + self._logger.error("Error rendering variables: %s", err) + return else: - variables = {} - - if run_variables: - variables.update(run_variables) + variables = run_variables if ( not skip_condition diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 1e0fad9be5d..eab30e01ee2 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -77,7 +77,7 @@ CONFIG_SCHEMA = vol.Schema( SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( - {vol.Optional(ATTR_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA} + {vol.Optional(ATTR_VARIABLES): {str: cv.match_all}} ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index a54f97ec7e5..602a8ebfd2a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -81,7 +81,10 @@ from homeassistant.const import ( ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template as template_helper +from homeassistant.helpers import ( + script_variables as script_variables_helper, + template as template_helper, +) from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify import homeassistant.util.dt as dt_util @@ -863,7 +866,11 @@ def make_entity_service_schema( ) -SCRIPT_VARIABLES_SCHEMA = vol.Schema({str: template_complex}) +SCRIPT_VARIABLES_SCHEMA = vol.All( + vol.Schema({str: template_complex}), + # pylint: disable=unnecessary-lambda + lambda val: script_variables_helper.ScriptVariables(val), +) def script_action(value: Any) -> dict: diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index cd664974431..bd1442587eb 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -55,6 +55,7 @@ from homeassistant.const import ( from homeassistant.core import SERVICE_CALL_LIMIT, Context, HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, template from homeassistant.helpers.event import async_call_later, async_track_template +from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.service import ( CONF_SERVICE_DATA, async_prepare_call_from_config, @@ -717,7 +718,7 @@ class Script: logger: Optional[logging.Logger] = None, log_exceptions: bool = True, top_level: bool = True, - variables: Optional[Dict[str, Any]] = None, + variables: Optional[ScriptVariables] = None, ) -> None: """Initialize the script.""" all_scripts = hass.data.get(DATA_SCRIPTS) @@ -900,15 +901,19 @@ class Script: # during the run back to the caller. if self._top_level: if self.variables: - if self._variables_dynamic: - variables = template.render_complex(self.variables, run_variables) - else: - variables = dict(self.variables) + try: + variables = self.variables.async_render( + self._hass, + run_variables, + ) + except template.TemplateError as err: + self._log("Error rendering variables: %s", err, level=logging.ERROR) + raise + elif run_variables: + variables = dict(run_variables) else: variables = {} - if run_variables: - variables.update(run_variables) variables["context"] = context else: variables = cast(dict, run_variables) diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py new file mode 100644 index 00000000000..001c3b8667c --- /dev/null +++ b/homeassistant/helpers/script_variables.py @@ -0,0 +1,57 @@ +"""Script variables.""" +from typing import Any, Dict, Mapping, Optional + +from homeassistant.core import HomeAssistant, callback + +from . import template + + +class ScriptVariables: + """Class to hold and render script variables.""" + + def __init__(self, variables: Dict[str, Any]): + """Initialize script variables.""" + self.variables = variables + self._has_template: Optional[bool] = None + + @callback + def async_render( + self, + hass: HomeAssistant, + run_variables: Optional[Mapping[str, Any]], + ) -> Dict[str, Any]: + """Render script variables. + + The run variables are used to compute the static variables, but afterwards will also + be merged on top of the static variables. + """ + if self._has_template is None: + self._has_template = template.is_complex(self.variables) + template.attach(hass, self.variables) + + if not self._has_template: + rendered_variables = dict(self.variables) + + if run_variables is not None: + rendered_variables.update(run_variables) + + return rendered_variables + + rendered_variables = {} if run_variables is None else dict(run_variables) + + for key, value in self.variables.items(): + # We can skip if we're going to override this key with + # run variables anyway + if key in rendered_variables: + continue + + rendered_variables[key] = template.render_complex(value, rendered_variables) + + if run_variables: + rendered_variables.update(run_variables) + + return rendered_variables + + def as_dict(self) -> dict: + """Return dict version of this class.""" + return self.variables diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 5ee0ff62af2..9c38574945d 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1136,7 +1136,7 @@ async def test_logbook_humanify_automation_triggered_event(hass): assert event2["entity_id"] == "automation.bye" -async def test_automation_variables(hass): +async def test_automation_variables(hass, caplog): """Test automation variables.""" calls = async_mock_service(hass, "test", "automation") @@ -1172,6 +1172,15 @@ async def test_automation_variables(hass): "service": "test.automation", }, }, + { + "variables": { + "test_var": "{{ trigger.event.data.break + 1 }}", + }, + "trigger": {"platform": "event", "event_type": "test_event_3"}, + "action": { + "service": "test.automation", + }, + }, ] }, ) @@ -1188,3 +1197,13 @@ async def test_automation_variables(hass): hass.bus.async_fire("test_event_2", {"pass_condition": True}) await hass.async_block_till_done() assert len(calls) == 2 + + assert "Error rendering variables" not in caplog.text + hass.bus.async_fire("test_event_3") + await hass.async_block_till_done() + assert len(calls) == 2 + assert "Error rendering variables" in caplog.text + + hass.bus.async_fire("test_event_3", {"break": 0}) + await hass.async_block_till_done() + assert len(calls) == 3 diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 5fb832d0f36..152c74d8fe9 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, callback, split_entity_id from homeassistant.exceptions import ServiceNotFound +from homeassistant.helpers import template from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import bind_hass @@ -617,7 +618,7 @@ async def test_concurrent_script(hass, concurrently): assert not script.is_on(hass, "script.script2") -async def test_script_variables(hass): +async def test_script_variables(hass, caplog): """Test defining scripts.""" assert await async_setup_component( hass, @@ -652,6 +653,19 @@ async def test_script_variables(hass): }, ], }, + "script3": { + "variables": { + "test_var": "{{ break + 1 }}", + }, + "sequence": [ + { + "service": "test.script", + "data": { + "value": "{{ test_var }}", + }, + }, + ], + }, } }, ) @@ -681,3 +695,14 @@ async def test_script_variables(hass): assert len(mock_calls) == 3 assert mock_calls[2].data["value"] == "from_service" + + assert "Error rendering variables" not in caplog.text + with pytest.raises(template.TemplateError): + await hass.services.async_call("script", "script3", blocking=True) + assert "Error rendering variables" in caplog.text + assert len(mock_calls) == 3 + + await hass.services.async_call("script", "script3", {"break": 0}, blocking=True) + + assert len(mock_calls) == 4 + assert mock_calls[3].data["value"] == "1" diff --git a/tests/helpers/test_script_variables.py b/tests/helpers/test_script_variables.py new file mode 100644 index 00000000000..6e671d14a23 --- /dev/null +++ b/tests/helpers/test_script_variables.py @@ -0,0 +1,60 @@ +"""Test script variables.""" +import pytest + +from homeassistant.helpers import config_validation as cv, template + + +async def test_static_vars(): + """Test static vars.""" + orig = {"hello": "world"} + var = cv.SCRIPT_VARIABLES_SCHEMA(orig) + rendered = var.async_render(None, None) + assert rendered is not orig + assert rendered == orig + + +async def test_static_vars_run_args(): + """Test static vars.""" + orig = {"hello": "world"} + orig_copy = dict(orig) + var = cv.SCRIPT_VARIABLES_SCHEMA(orig) + rendered = var.async_render(None, {"hello": "override", "run": "var"}) + assert rendered == {"hello": "override", "run": "var"} + # Make sure we don't change original vars + assert orig == orig_copy + + +async def test_template_vars(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ 1 + 1 }}"}) + rendered = var.async_render(hass, None) + assert rendered == {"hello": "2"} + + +async def test_template_vars_run_args(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA( + { + "something": "{{ run_var_ex + 1 }}", + "something_2": "{{ run_var_ex + 1 }}", + } + ) + rendered = var.async_render( + hass, + { + "run_var_ex": 5, + "something_2": 1, + }, + ) + assert rendered == { + "run_var_ex": 5, + "something": "6", + "something_2": 1, + } + + +async def test_template_vars_error(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ canont.work }}"}) + with pytest.raises(template.TemplateError): + var.async_render(hass, None) From b1b7944012167c59dbf957225037e67086665198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Fri, 11 Sep 2020 13:16:25 +0200 Subject: [PATCH 793/862] Set variable values in scripts (#39915) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/config_validation.py | 13 ++++++ homeassistant/helpers/script.py | 9 ++++ homeassistant/helpers/script_variables.py | 25 +++++++---- tests/helpers/test_script.py | 39 ++++++++++++++++ tests/helpers/test_script_variables.py | 52 ++++++++++++++++++++++ 5 files changed, 129 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 602a8ebfd2a..282e63e6440 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -67,6 +67,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, CONF_UNTIL, CONF_VALUE_TEMPLATE, + CONF_VARIABLES, CONF_WAIT_FOR_TRIGGER, CONF_WAIT_TEMPLATE, CONF_WHILE, @@ -1127,6 +1128,13 @@ _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA = vol.Schema( } ) +_SCRIPT_SET_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ALIAS): string, + vol.Required(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA, + } +) + SCRIPT_ACTION_DELAY = "delay" SCRIPT_ACTION_WAIT_TEMPLATE = "wait_template" SCRIPT_ACTION_CHECK_CONDITION = "condition" @@ -1137,6 +1145,7 @@ SCRIPT_ACTION_ACTIVATE_SCENE = "scene" SCRIPT_ACTION_REPEAT = "repeat" SCRIPT_ACTION_CHOOSE = "choose" SCRIPT_ACTION_WAIT_FOR_TRIGGER = "wait_for_trigger" +SCRIPT_ACTION_VARIABLES = "variables" def determine_script_action(action: dict) -> str: @@ -1168,6 +1177,9 @@ def determine_script_action(action: dict) -> str: if CONF_WAIT_FOR_TRIGGER in action: return SCRIPT_ACTION_WAIT_FOR_TRIGGER + if CONF_VARIABLES in action: + return SCRIPT_ACTION_VARIABLES + return SCRIPT_ACTION_CALL_SERVICE @@ -1182,4 +1194,5 @@ ACTION_TYPE_SCHEMAS: Dict[str, Callable[[Any], dict]] = { SCRIPT_ACTION_REPEAT: _SCRIPT_REPEAT_SCHEMA, SCRIPT_ACTION_CHOOSE: _SCRIPT_CHOOSE_SCHEMA, SCRIPT_ACTION_WAIT_FOR_TRIGGER: _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA, + SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA, } diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index bd1442587eb..717e9c3980c 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -46,6 +46,7 @@ from homeassistant.const import ( CONF_SEQUENCE, CONF_TIMEOUT, CONF_UNTIL, + CONF_VARIABLES, CONF_WAIT_FOR_TRIGGER, CONF_WAIT_TEMPLATE, CONF_WHILE, @@ -612,6 +613,14 @@ class _ScriptRun: task.cancel() remove_triggers() + async def _async_variables_step(self): + """Set a variable value.""" + self._script.last_action = self._action.get(CONF_ALIAS, "setting variables") + self._log("Executing step %s", self._script.last_action) + self._variables = self._action[CONF_VARIABLES].async_render( + self._hass, self._variables, render_as_defaults=False + ) + async def _async_run_script(self, script): """Execute a script.""" await self._async_run_long_action( diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index 001c3b8667c..3140fc4dced 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -19,21 +19,31 @@ class ScriptVariables: self, hass: HomeAssistant, run_variables: Optional[Mapping[str, Any]], + *, + render_as_defaults: bool = True, ) -> Dict[str, Any]: """Render script variables. - The run variables are used to compute the static variables, but afterwards will also - be merged on top of the static variables. + The run variables are used to compute the static variables. + + If `render_as_defaults` is True, the run variables will not be overridden. + """ if self._has_template is None: self._has_template = template.is_complex(self.variables) template.attach(hass, self.variables) if not self._has_template: - rendered_variables = dict(self.variables) + if render_as_defaults: + rendered_variables = dict(self.variables) - if run_variables is not None: - rendered_variables.update(run_variables) + if run_variables is not None: + rendered_variables.update(run_variables) + else: + rendered_variables = ( + {} if run_variables is None else dict(run_variables) + ) + rendered_variables.update(self.variables) return rendered_variables @@ -42,14 +52,11 @@ class ScriptVariables: for key, value in self.variables.items(): # We can skip if we're going to override this key with # run variables anyway - if key in rendered_variables: + if render_as_defaults and key in rendered_variables: continue rendered_variables[key] = template.render_complex(value, rendered_variables) - if run_variables: - rendered_variables.update(run_variables) - return rendered_variables def as_dict(self) -> dict: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index d298283d11e..0bd353e1fa0 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1785,3 +1785,42 @@ async def test_started_action(hass, caplog): await hass.async_block_till_done() assert log_message in caplog.text + + +async def test_set_variable(hass, caplog): + """Test setting variables in scripts.""" + sequence = cv.SCRIPT_SCHEMA( + [ + {"variables": {"variable": "value"}}, + {"service": "test.script", "data": {"value": "{{ variable }}"}}, + ] + ) + script_obj = script.Script(hass, sequence, "test script", "test_domain") + + mock_calls = async_mock_service(hass, "test", "script") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert mock_calls[0].data["value"] == "value" + + +async def test_set_redefines_variable(hass, caplog): + """Test setting variables based on their current value.""" + sequence = cv.SCRIPT_SCHEMA( + [ + {"variables": {"variable": "1"}}, + {"service": "test.script", "data": {"value": "{{ variable }}"}}, + {"variables": {"variable": "{{ variable | int + 1 }}"}}, + {"service": "test.script", "data": {"value": "{{ variable }}"}}, + ] + ) + script_obj = script.Script(hass, sequence, "test script", "test_domain") + + mock_calls = async_mock_service(hass, "test", "script") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert mock_calls[0].data["value"] == "1" + assert mock_calls[1].data["value"] == "2" diff --git a/tests/helpers/test_script_variables.py b/tests/helpers/test_script_variables.py index 6e671d14a23..20a70cb33eb 100644 --- a/tests/helpers/test_script_variables.py +++ b/tests/helpers/test_script_variables.py @@ -24,6 +24,28 @@ async def test_static_vars_run_args(): assert orig == orig_copy +async def test_static_vars_no_default(): + """Test static vars.""" + orig = {"hello": "world"} + var = cv.SCRIPT_VARIABLES_SCHEMA(orig) + rendered = var.async_render(None, None, render_as_defaults=False) + assert rendered is not orig + assert rendered == orig + + +async def test_static_vars_run_args_no_default(): + """Test static vars.""" + orig = {"hello": "world"} + orig_copy = dict(orig) + var = cv.SCRIPT_VARIABLES_SCHEMA(orig) + rendered = var.async_render( + None, {"hello": "override", "run": "var"}, render_as_defaults=False + ) + assert rendered == {"hello": "world", "run": "var"} + # Make sure we don't change original vars + assert orig == orig_copy + + async def test_template_vars(hass): """Test template vars.""" var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ 1 + 1 }}"}) @@ -53,6 +75,36 @@ async def test_template_vars_run_args(hass): } +async def test_template_vars_no_default(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ 1 + 1 }}"}) + rendered = var.async_render(hass, None, render_as_defaults=False) + assert rendered == {"hello": "2"} + + +async def test_template_vars_run_args_no_default(hass): + """Test template vars.""" + var = cv.SCRIPT_VARIABLES_SCHEMA( + { + "something": "{{ run_var_ex + 1 }}", + "something_2": "{{ run_var_ex + 1 }}", + } + ) + rendered = var.async_render( + hass, + { + "run_var_ex": 5, + "something_2": 1, + }, + render_as_defaults=False, + ) + assert rendered == { + "run_var_ex": 5, + "something": "6", + "something_2": "6", + } + + async def test_template_vars_error(hass): """Test template vars.""" var = cv.SCRIPT_VARIABLES_SCHEMA({"hello": "{{ canont.work }}"}) From 5201410e39e8d77bae52402ac7c14536403a5c8d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Sep 2020 23:07:23 +0200 Subject: [PATCH 794/862] Bump aioshelly to 0.3.1 (#39917) --- homeassistant/components/shelly/manifest.json | 2 +- homeassistant/components/shelly/switch.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 1c12125fd89..b2d3a7b7795 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.3.0"], + "requirements": ["aioshelly==0.3.1"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu"] } diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 5550240478f..0aaa6dbc911 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -1,5 +1,5 @@ """Switch for Shelly.""" -from aioshelly import RelayBlock +from aioshelly import Block from homeassistant.components.switch import SwitchEntity from homeassistant.core import callback @@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RelaySwitch(ShellyBlockEntity, SwitchEntity): """Switch that controls a relay block on Shelly devices.""" - def __init__(self, wrapper: ShellyDeviceWrapper, block: RelayBlock) -> None: + def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None: """Initialize relay switch.""" super().__init__(wrapper, block) self.control_result = None diff --git a/requirements_all.txt b/requirements_all.txt index b875fa5e4a2..dc1e4bd8951 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.3.0 +aioshelly==0.3.1 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69b6d51416f..952d4018989 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.3.0 +aioshelly==0.3.1 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From 758e60a58de35247f6a71e87c003693129bb795d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Sep 2020 16:19:11 -0500 Subject: [PATCH 795/862] Prevent missing integration from failing HomeKit startup (#39918) --- homeassistant/components/homekit/__init__.py | 11 ++- tests/components/homekit/test_homekit.py | 93 +++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index e77d4ef134e..acc96397601 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -38,7 +38,7 @@ from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA from homeassistant.helpers.reload import async_integration_yaml_config -from homeassistant.loader import async_get_integration +from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.util import get_local_ip from .accessories import get_accessory @@ -712,8 +712,13 @@ class HomeKit: if dev_reg_ent.sw_version: ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version if ATTR_MANUFACTURER not in ent_cfg: - integration = await async_get_integration(self.hass, ent_reg_ent.platform) - ent_cfg[ATTR_INTERGRATION] = integration.name + try: + integration = await async_get_integration( + self.hass, ent_reg_ent.platform + ) + ent_cfg[ATTR_INTERGRATION] = integration.name + except IntegrationNotFound: + ent_cfg[ATTR_INTERGRATION] = ent_reg_ent.platform class HomeKitPairingQRView(HomeAssistantView): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 16473cd7b22..757281af1e9 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -807,6 +807,91 @@ async def test_homekit_finds_linked_batteries( ) +async def test_homekit_async_get_integration_fails( + hass, hk_driver, debounce_patcher, device_reg, entity_reg +): + """Test that we continue if async_get_integration fails.""" + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {"light.demo": {}}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + entry_id=entry.entry_id, + ) + homekit.driver = hk_driver + # pylint: disable=protected-access + homekit._filter = Mock(return_value=True) + homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + sw_version="0.16.0", + model="Powerwall 2", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + binary_charging_sensor = entity_reg.async_get_or_create( + "binary_sensor", + "invalid_integration_does_not_exist", + "battery_charging", + device_id=device_entry.id, + device_class=DEVICE_CLASS_BATTERY_CHARGING, + ) + battery_sensor = entity_reg.async_get_or_create( + "sensor", + "invalid_integration_does_not_exist", + "battery", + device_id=device_entry.id, + device_class=DEVICE_CLASS_BATTERY, + ) + light = entity_reg.async_get_or_create( + "light", "invalid_integration_does_not_exist", "demo", device_id=device_entry.id + ) + + hass.states.async_set( + binary_charging_sensor.entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING}, + ) + hass.states.async_set( + battery_sensor.entity_id, 30, {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} + ) + hass.states.async_set(light.entity_id, STATE_ON) + + def _mock_get_accessory(*args, **kwargs): + return [None, "acc", None] + + with patch.object(homekit.bridge, "add_accessory"), patch( + f"{PATH_HOMEKIT}.show_setup_message" + ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( + "pyhap.accessory_driver.AccessoryDriver.start_service" + ): + await homekit.async_start() + await hass.async_block_till_done() + + mock_get_acc.assert_called_with( + hass, + hk_driver, + ANY, + ANY, + { + "model": "Powerwall 2", + "sw_version": "0.16.0", + "platform": "invalid_integration_does_not_exist", + "linked_battery_charging_sensor": "binary_sensor.invalid_integration_does_not_exist_battery_charging", + "linked_battery_sensor": "sensor.invalid_integration_does_not_exist_battery", + }, + ) + + async def test_setup_imported(hass): """Test async_setup with imported config options.""" legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) @@ -1222,7 +1307,13 @@ async def test_reload(hass): ) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), patch( f"{PATH_HOMEKIT}.HomeKit" - ) as mock_homekit2: + ) as mock_homekit2, patch.object(homekit.bridge, "add_accessory"), patch( + f"{PATH_HOMEKIT}.show_setup_message" + ), patch( + f"{PATH_HOMEKIT}.get_accessory" + ), patch( + "pyhap.accessory_driver.AccessoryDriver.start_service" + ): mock_homekit2.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() await hass.services.async_call( From 3fbde22cc41c06e7f05adc8772aa8ad965b41ec6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 00:15:13 -0500 Subject: [PATCH 796/862] Update zeroconf to 0.28.5 (#39923) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index c703283e38d..7ac2cf9c5f1 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.4"], + "requirements": ["zeroconf==0.28.5"], "dependencies": ["api"], "codeowners": ["@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 53f22d3787b..afbec0dd3d0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ sqlalchemy==1.3.19 voluptuous-serialize==2.4.0 voluptuous==0.11.7 yarl==1.4.2 -zeroconf==0.28.4 +zeroconf==0.28.5 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index dc1e4bd8951..3efe49e0f7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2287,7 +2287,7 @@ youtube_dl==2020.07.28 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.4 +zeroconf==0.28.5 # homeassistant.components.zha zha-quirks==0.0.44 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 952d4018989..c17fc21d7a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1053,7 +1053,7 @@ xmltodict==0.12.0 yeelight==0.5.3 # homeassistant.components.zeroconf -zeroconf==0.28.4 +zeroconf==0.28.5 # homeassistant.components.zha zha-quirks==0.0.44 From db64a9ebfa5fe90ab98ae3067ca38a3a9bd16d24 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Sep 2020 13:00:00 +0200 Subject: [PATCH 797/862] Accept known hosts for get_url for OAuth (#39936) --- homeassistant/helpers/network.py | 32 +++++++++++++++++++ tests/helpers/test_network.py | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index d40fd9fad2b..8bdfc286c1a 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -75,6 +75,38 @@ def get_url( except NoURLAvailableError: pass + # For current request, we accept loopback interfaces (e.g., 127.0.0.1), + # the Supervisor hostname and localhost transparently + request_host = _get_request_host() + if ( + require_current_request + and request_host is not None + and hass.config.api is not None + ): + scheme = "https" if hass.config.api.use_ssl else "http" + current_url = yarl.URL.build( + scheme=scheme, host=request_host, port=hass.config.api.port + ) + + known_hostname = None + if hass.components.hassio.is_hassio(): + host_info = hass.components.hassio.get_host_info() + known_hostname = f"{host_info['hostname']}.local" + + if ( + ( + ( + allow_ip + and is_ip_address(request_host) + and is_loopback(ip_address(request_host)) + ) + or request_host in ["localhost", known_hostname] + ) + and (not require_ssl or current_url.scheme == "https") + and (not require_standard_port or current_url.is_default_port()) + ): + return normalize_url(str(current_url)) + # We have to be honest now, we have no viable option available raise NoURLAvailableError diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index f51ee2090dc..ed97b3e3757 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -15,6 +15,7 @@ from homeassistant.helpers.network import ( ) from tests.async_mock import Mock, patch +from tests.common import mock_component async def test_get_url_internal(hass: HomeAssistant): @@ -799,3 +800,55 @@ async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant): assert _get_external_url(hass, allow_ip=False) == "https://example.com" assert _get_external_url(hass, require_standard_port=True) == "https://example.com" assert _get_external_url(hass, require_ssl=True) == "https://example.com" + + +async def test_get_current_request_url_with_known_host( + hass: HomeAssistant, current_request +): + """Test getting current request URL with known hosts addresses.""" + hass.config.api = Mock( + use_ssl=False, port=8123, local_ip="127.0.0.1", deprecated_base_url=None + ) + assert hass.config.internal_url is None + + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True) + + # Ensure we accept localhost + with patch( + "homeassistant.helpers.network._get_request_host", return_value="localhost" + ): + assert get_url(hass, require_current_request=True) == "http://localhost:8123" + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, require_ssl=True) + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, require_standard_port=True) + + # Ensure we accept local loopback ip (e.g., 127.0.0.1) + with patch( + "homeassistant.helpers.network._get_request_host", return_value="127.0.0.8" + ): + assert get_url(hass, require_current_request=True) == "http://127.0.0.8:8123" + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, allow_ip=False) + + # Ensure hostname from Supervisor is accepted transparently + mock_component(hass, "hassio") + hass.components.hassio.is_hassio = Mock(return_value=True) + hass.components.hassio.get_host_info = Mock( + return_value={"hostname": "homeassistant"} + ) + + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="homeassistant.local", + ): + assert ( + get_url(hass, require_current_request=True) + == "http://homeassistant.local:8123" + ) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="unknown.local" + ), pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True) From a002e9b12fb6dad3a97f9f8ca06131fa3e4017c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 11 Sep 2020 12:18:53 +0000 Subject: [PATCH 798/862] Bumped version to 0.115.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7a32ea1ee58..2eb1d3c6d83 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b5" +PATCH_VERSION = "0b6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 18be6cbadc73ed70d44565fd99fdede3dfacd130 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 12 Sep 2020 15:22:14 +0300 Subject: [PATCH 799/862] Handle Kodi shutdown (#39856) * Handle Kodi shutdown * Core review comments * Make async_on_quit a coroutine --- homeassistant/components/kodi/manifest.json | 2 +- homeassistant/components/kodi/media_player.py | 26 ++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index b3794d5dfa2..da4daf85ada 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,7 +2,7 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", - "requirements": ["pykodi==0.1.2"], + "requirements": ["pykodi==0.2.0"], "codeowners": [ "@OnFreund" ], diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index ce05f3fc732..f13d5301625 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -5,6 +5,7 @@ import logging import re import jsonrpc_base +from pykodi import CannotConnectError import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -324,11 +325,15 @@ class KodiEntity(MediaPlayerEntity): self._app_properties["muted"] = data["muted"] self.async_write_ha_state() - @callback - def async_on_quit(self, sender, data): + async def async_on_quit(self, sender, data): """Reset the player state on quit action.""" + await self._clear_connection() + + async def _clear_connection(self, close=True): self._reset_state() - self.hass.async_create_task(self._connection.close()) + self.async_write_ha_state() + if close: + await self._connection.close() @property def unique_id(self): @@ -386,14 +391,23 @@ class KodiEntity(MediaPlayerEntity): try: await self._connection.connect() self._on_ws_connected() - except jsonrpc_base.jsonrpc.TransportError: - _LOGGER.info("Unable to connect to Kodi via websocket") + except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): _LOGGER.debug("Unable to connect to Kodi via websocket", exc_info=True) + await self._clear_connection(False) + + async def _ping(self): + try: + await self._kodi.ping() + except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError): + _LOGGER.debug("Unable to ping Kodi via websocket", exc_info=True) + await self._clear_connection() async def _async_connect_websocket_if_disconnected(self, *_): """Reconnect the websocket if it fails.""" if not self._connection.connected: await self._async_ws_connect() + else: + await self._ping() @callback def _register_ws_callbacks(self): @@ -464,7 +478,7 @@ class KodiEntity(MediaPlayerEntity): @property def should_poll(self): """Return True if entity has to be polled for state.""" - return (not self._connection.can_subscribe) or (not self._connection.connected) + return not self._connection.can_subscribe @property def volume_level(self): diff --git a/requirements_all.txt b/requirements_all.txt index 3efe49e0f7a..2fcd10be276 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1431,7 +1431,7 @@ pyitachip2ir==0.0.7 pykira==0.1.1 # homeassistant.components.kodi -pykodi==0.1.2 +pykodi==0.2.0 # homeassistant.components.kwb pykwb==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c17fc21d7a2..f07db06eff0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -692,7 +692,7 @@ pyisy==2.0.2 pykira==0.1.1 # homeassistant.components.kodi -pykodi==0.1.2 +pykodi==0.2.0 # homeassistant.components.lastfm pylast==3.3.0 From 3240be0bb6d6b2012361a94ea5f8a2eac5a123fd Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Fri, 11 Sep 2020 19:47:48 +0100 Subject: [PATCH 800/862] Bump pyloopenergy library to 0.2.1 (#39919) --- CODEOWNERS | 1 + homeassistant/components/loopenergy/manifest.json | 6 ++++-- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 70d89c5e45e..42c39544540 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -236,6 +236,7 @@ homeassistant/components/linux_battery/* @fabaff homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd +homeassistant/components/loopenergy/* @pavoni homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json index cf7343af6a4..9b421083d10 100644 --- a/homeassistant/components/loopenergy/manifest.json +++ b/homeassistant/components/loopenergy/manifest.json @@ -2,6 +2,8 @@ "domain": "loopenergy", "name": "Loop Energy", "documentation": "https://www.home-assistant.io/integrations/loopenergy", - "requirements": ["pyloopenergy==0.1.3"], - "codeowners": [] + "requirements": ["pyloopenergy==0.2.1"], + "codeowners": [ + "@pavoni" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index 2fcd10be276..4c76d1c2d2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1455,7 +1455,7 @@ pylibrespot-java==0.1.0 pylitejet==0.1 # homeassistant.components.loopenergy -pyloopenergy==0.1.3 +pyloopenergy==0.2.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.6.1 From f81606cbf58ca5acba39b47a3fd861ceab626951 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2020 13:18:40 -0500 Subject: [PATCH 801/862] Return the listeners with the template result for the websocket api (#39925) --- .../components/websocket_api/commands.py | 8 +- homeassistant/helpers/event.py | 9 ++ .../components/websocket_api/test_commands.py | 25 ++++-- tests/helpers/test_event.py | 87 +++++++++++++++++-- 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 04ad0ae3d3a..036cd690da2 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -253,9 +253,11 @@ def handle_render_template(hass, connection, msg): template.hass = hass variables = msg.get("variables") + info = None @callback def _template_listener(event, updates): + nonlocal info track_template_result = updates.pop() result = track_template_result.result if isinstance(result, TemplateError): @@ -267,7 +269,11 @@ def handle_render_template(hass, connection, msg): result = None - connection.send_message(messages.event_message(msg["id"], {"result": result})) + connection.send_message( + messages.event_message( + msg["id"], {"result": result, "listeners": info.listeners} # type: ignore + ) + ) info = async_track_template_result( hass, [TrackTemplate(template, variables)], _template_listener diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d9f1b8d9681..52ebe2d4de4 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -527,6 +527,15 @@ class _TrackTemplateResultInfo: self._last_info = self._info.copy() self._create_listeners() + @property + def listeners(self) -> Dict: + """State changes that will cause a re-render.""" + return { + "all": self._all_listener is not None, + "entities": self._last_entities, + "domains": self._last_domains, + } + @property def _needs_all_listener(self) -> bool: for track_template_ in self._track_templates: diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 4113a833872..1b9eea86018 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -420,14 +420,20 @@ async def test_render_template_renders_template( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: on"} + assert event == { + "result": "State is: on", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } hass.states.async_set("light.test", "off") msg = await websocket_client.receive_json() assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: off"} + assert event == { + "result": "State is: off", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } async def test_render_template_manual_entity_ids_no_longer_needed( @@ -453,14 +459,20 @@ async def test_render_template_manual_entity_ids_no_longer_needed( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: on"} + assert event == { + "result": "State is: on", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } hass.states.async_set("light.test", "off") msg = await websocket_client.receive_json() assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": "State is: off"} + assert event == { + "result": "State is: off", + "listeners": {"all": False, "domains": [], "entities": ["light.test"]}, + } async def test_render_template_with_error( @@ -480,7 +492,10 @@ async def test_render_template_with_error( assert msg["id"] == 5 assert msg["type"] == "event" event = msg["event"] - assert event == {"result": None} + assert event == { + "result": None, + "listeners": {"all": True, "domains": [], "entities": []}, + } assert "my_unknown_var" in caplog.text assert "TemplateError" in caplog.text diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index cc06c0fd19c..ba14c8a757f 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -682,20 +682,33 @@ async def test_track_template_result_complex(hass): hass.states.async_set("light.one", "on") hass.states.async_set("lock.one", "locked") - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template_complex, None)], specific_run_callback ) await hass.async_block_till_done() + assert info.listeners == {"all": True, "domains": set(), "entities": set()} + hass.states.async_set("sensor.domain", "light") await hass.async_block_till_done() assert len(specific_runs) == 1 assert specific_runs[0].strip() == "['light.one']" + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } + hass.states.async_set("sensor.domain", "lock") await hass.async_block_till_done() assert len(specific_runs) == 2 assert specific_runs[1].strip() == "['lock.one']" + assert info.listeners == { + "all": False, + "domains": {"lock"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("sensor.domain", "all") await hass.async_block_till_done() @@ -703,11 +716,17 @@ async def test_track_template_result_complex(hass): assert "light.one" in specific_runs[2] assert "lock.one" in specific_runs[2] assert "sensor.domain" in specific_runs[2] + assert info.listeners == {"all": True, "domains": set(), "entities": set()} hass.states.async_set("sensor.domain", "light") await hass.async_block_till_done() assert len(specific_runs) == 4 assert specific_runs[3].strip() == "['light.one']" + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("light.two", "on") await hass.async_block_till_done() @@ -715,6 +734,11 @@ async def test_track_template_result_complex(hass): assert "light.one" in specific_runs[4] assert "light.two" in specific_runs[4] assert "sensor.domain" not in specific_runs[4] + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("light.three", "on") await hass.async_block_till_done() @@ -723,26 +747,51 @@ async def test_track_template_result_complex(hass): assert "light.two" in specific_runs[5] assert "light.three" in specific_runs[5] assert "sensor.domain" not in specific_runs[5] + assert info.listeners == { + "all": False, + "domains": {"light"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("sensor.domain", "lock") await hass.async_block_till_done() assert len(specific_runs) == 7 assert specific_runs[6].strip() == "['lock.one']" + assert info.listeners == { + "all": False, + "domains": {"lock"}, + "entities": {"sensor.domain"}, + } hass.states.async_set("sensor.domain", "single_binary_sensor") await hass.async_block_till_done() assert len(specific_runs) == 8 assert specific_runs[7].strip() == "unknown" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"binary_sensor.single", "sensor.domain"}, + } hass.states.async_set("binary_sensor.single", "binary_sensor_on") await hass.async_block_till_done() assert len(specific_runs) == 9 assert specific_runs[8].strip() == "binary_sensor_on" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"binary_sensor.single", "sensor.domain"}, + } hass.states.async_set("sensor.domain", "lock") await hass.async_block_till_done() assert len(specific_runs) == 10 assert specific_runs[9].strip() == "['lock.one']" + assert info.listeners == { + "all": False, + "domains": {"lock"}, + "entities": {"sensor.domain"}, + } async def test_track_template_result_with_wildcard(hass): @@ -766,7 +815,7 @@ async def test_track_template_result_with_wildcard(hass): hass.states.async_set("cover.office_window", "closed") hass.states.async_set("cover.office_skylight", "open") - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template_complex, None)], specific_run_callback ) await hass.async_block_till_done() @@ -774,6 +823,7 @@ async def test_track_template_result_with_wildcard(hass): hass.states.async_set("cover.office_window", "open") await hass.async_block_till_done() assert len(specific_runs) == 1 + assert info.listeners == {"all": True, "domains": set(), "entities": set()} assert "cover.office_drapes=closed" in specific_runs[0] assert "cover.office_window=open" in specific_runs[0] @@ -808,11 +858,22 @@ async def test_track_template_result_with_group(hass): def specific_run_callback(event, updates): specific_runs.append(updates.pop().result) - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template_complex, None)], specific_run_callback ) await hass.async_block_till_done() + assert info.listeners == { + "all": False, + "domains": set(), + "entities": { + "group.power_sensors", + "sensor.power_1", + "sensor.power_2", + "sensor.power_3", + }, + } + hass.states.async_set("sensor.power_1", 100.1) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -851,10 +912,11 @@ async def test_track_template_result_and_conditional(hass): def specific_run_callback(event, updates): specific_runs.append(updates.pop().result) - async_track_template_result( + info = async_track_template_result( hass, [TrackTemplate(template, None)], specific_run_callback ) await hass.async_block_till_done() + assert info.listeners == {"all": False, "domains": set(), "entities": {"light.a"}} hass.states.async_set("light.b", "on") await hass.async_block_till_done() @@ -864,11 +926,21 @@ async def test_track_template_result_and_conditional(hass): await hass.async_block_till_done() assert len(specific_runs) == 1 assert specific_runs[0] == "on" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"light.a", "light.b"}, + } hass.states.async_set("light.b", "off") await hass.async_block_till_done() assert len(specific_runs) == 2 assert specific_runs[1] == "off" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"light.a", "light.b"}, + } hass.states.async_set("light.a", "off") await hass.async_block_till_done() @@ -924,7 +996,7 @@ async def test_track_template_result_iterator(hass): def filter_callback(event, updates): filter_runs.append(updates.pop().result) - async_track_template_result( + info = async_track_template_result( hass, [ TrackTemplate( @@ -939,6 +1011,11 @@ async def test_track_template_result_iterator(hass): filter_callback, ) await hass.async_block_till_done() + assert info.listeners == { + "all": False, + "domains": {"sensor"}, + "entities": {"sensor.test"}, + } hass.states.async_set("sensor.test", 6) await hass.async_block_till_done() From fcbcebea9ba7763edefca969e6124ad6ea3841ac Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 11 Sep 2020 16:50:17 +0200 Subject: [PATCH 802/862] Fix missing position attribute for MeteoFranceAlertSensor (#39938) --- homeassistant/components/meteo_france/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 1e4c9b1215f..3c88914aafd 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -79,9 +79,10 @@ class MeteoFranceSensor(CoordinatorEntity): """Initialize the Meteo-France sensor.""" super().__init__(coordinator) self._type = sensor_type - city_name = self.coordinator.data.position["name"] - self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}" - self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}" + if hasattr(self.coordinator.data, "position"): + city_name = self.coordinator.data.position["name"] + self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}" + self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}" @property def unique_id(self): From 30f9e1b479f61b95cdee6764da86ae58172263f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Sep 2020 07:20:21 -0500 Subject: [PATCH 803/862] Change template loop detection strategy to allow self-referencing updates when there are multiple templates (#39943) --- .../components/template/template_entity.py | 29 ++- homeassistant/helpers/event.py | 16 -- tests/components/template/test_sensor.py | 214 ++++++++++++++++-- 3 files changed, 220 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 2e42b28fbd2..632eeea8926 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -5,6 +5,7 @@ from typing import Any, Callable, List, Optional, Union import voluptuous as vol +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import EVENT_HOMEASSISTANT_START, CoreState, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -121,7 +122,6 @@ class TemplateEntity(Entity): """Template Entity.""" self._template_attrs = {} self._async_update = None - self._async_update_entity_ids_filter = None self._attribute_templates = attribute_templates self._attributes = {} self._availability_template = availability_template @@ -130,6 +130,7 @@ class TemplateEntity(Entity): self._entity_picture_template = entity_picture_template self._icon = None self._entity_picture = None + self._self_ref_update_count = 0 @property def should_poll(self): @@ -223,18 +224,34 @@ class TemplateEntity(Entity): updates: List[TrackTemplateResult], ) -> None: """Call back the results to the attributes.""" + if event: self.async_set_context(event.context) + entity_id = event and event.data.get(ATTR_ENTITY_ID) + + if entity_id and entity_id == self.entity_id: + self._self_ref_update_count += 1 + else: + self._self_ref_update_count = 0 + + # If we need to make this less sensitive in the future, + # change the '>=' to a '>' here. + if self._self_ref_update_count >= len(self._template_attrs): + for update in updates: + _LOGGER.warning( + "Template loop detected while processing event: %s, skipping template render for Template[%s]", + event, + update.template.template, + ) + return + for update in updates: for attr in self._template_attrs[update.template]: attr.handle_result( event, update.template, update.last_result, update.result ) - if self._async_update_entity_ids_filter: - self._async_update_entity_ids_filter({self.entity_id}) - if self._async_update: self.async_write_ha_state() @@ -249,12 +266,8 @@ class TemplateEntity(Entity): ) self.async_on_remove(result_info.async_remove) result_info.async_refresh() - result_info.async_update_entity_ids_filter({self.entity_id}) self.async_write_ha_state() self._async_update = result_info.async_refresh - self._async_update_entity_ids_filter = ( - result_info.async_update_entity_ids_filter - ) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 52ebe2d4de4..435a265d9e0 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -508,7 +508,6 @@ class _TrackTemplateResultInfo: self._info: Dict[Template, RenderInfo] = {} self._last_domains: Set = set() self._last_entities: Set = set() - self._entity_ids_filter: Set = set() def async_setup(self) -> None: """Activation of template tracking.""" @@ -669,27 +668,12 @@ class _TrackTemplateResultInfo: """Force recalculate the template.""" self._refresh(None) - @callback - def async_update_entity_ids_filter(self, entity_ids: Set) -> None: - """Update the filtered entity_ids.""" - self._entity_ids_filter = entity_ids - @callback def _refresh(self, event: Optional[Event]) -> None: entity_id = event and event.data.get(ATTR_ENTITY_ID) updates = [] info_changed = False - if entity_id and entity_id in self._entity_ids_filter: - # Skip self-referencing updates - for track_template_ in self._track_templates: - _LOGGER.warning( - "Template loop detected while processing event: %s, skipping template render for Template[%s]", - event, - track_template_.template.template, - ) - return - for track_template_ in self._track_templates: template = track_template_.template if ( diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 439f154b4af..6c9bfa7e632 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -4,6 +4,8 @@ from unittest.mock import patch from homeassistant.bootstrap import async_from_config_dict from homeassistant.const import ( + ATTR_ENTITY_PICTURE, + ATTR_ICON, EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START, STATE_OFF, @@ -763,12 +765,6 @@ async def test_sun_renders_once_per_sensor(hass): async def test_self_referencing_sensor_loop(hass, caplog): """Test a self referencing sensor does not loop forever.""" - template_str = """ -{% for state in states -%} - {{ state.last_updated }} -{%- endfor %} -""" - await async_setup_component( hass, "sensor", @@ -777,7 +773,7 @@ async def test_self_referencing_sensor_loop(hass, caplog): "platform": "template", "sensors": { "test": { - "value_template": template_str, + "value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}", }, }, } @@ -790,15 +786,203 @@ async def test_self_referencing_sensor_loop(hass, caplog): assert len(hass.states.async_all()) == 1 - value = hass.states.get("sensor.test").state await hass.async_block_till_done() - - value2 = hass.states.get("sensor.test").state - assert value2 == value - await hass.async_block_till_done() - value3 = hass.states.get("sensor.test").state - assert value3 == value2 - assert "Template loop detected" in caplog.text + + state = hass.states.get("sensor.test") + assert int(state.state) == 1 + await hass.async_block_till_done() + assert int(state.state) == 1 + + +async def test_self_referencing_sensor_with_icon_loop(hass, caplog): + """Test a self referencing sensor loops forever with a valid self referencing icon.""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}", + "icon_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}mdi:greater{% else %}mdi:less{% endif %}", + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert "Template loop detected" in caplog.text + + state = hass.states.get("sensor.test") + assert int(state.state) == 2 + assert state.attributes[ATTR_ICON] == "mdi:greater" + + await hass.async_block_till_done() + assert int(state.state) == 2 + + +async def test_self_referencing_sensor_with_icon_and_picture_entity_loop(hass, caplog): + """Test a self referencing sensor loop forevers with a valid self referencing icon.""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": "{{ ((states.sensor.test.state or 0) | int) + 1 }}", + "icon_template": "{% if ((states.sensor.test.state or 0) | int) > 3 %}mdi:greater{% else %}mdi:less{% endif %}", + "entity_picture_template": "{% if ((states.sensor.test.state or 0) | int) >= 1 %}bigpic{% else %}smallpic{% endif %}", + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert "Template loop detected" in caplog.text + + state = hass.states.get("sensor.test") + assert int(state.state) == 3 + assert state.attributes[ATTR_ICON] == "mdi:less" + assert state.attributes[ATTR_ENTITY_PICTURE] == "bigpic" + + await hass.async_block_till_done() + assert int(state.state) == 3 + + +async def test_self_referencing_entity_picture_loop(hass, caplog): + """Test a self referencing sensor does not loop forever with a looping self referencing entity picture.""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test": { + "value_template": "{{ 1 }}", + "entity_picture_template": "{{ ((states.sensor.test.attributes['entity_picture'] or 0) | int) + 1 }}", + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert "Template loop detected" in caplog.text + + state = hass.states.get("sensor.test") + assert int(state.state) == 1 + assert state.attributes[ATTR_ENTITY_PICTURE] == "1" + + await hass.async_block_till_done() + assert int(state.state) == 1 + + +async def test_self_referencing_icon_with_no_loop(hass, caplog): + """Test a self referencing icon that does not loop.""" + + hass.states.async_set("sensor.heartworm_high_80", 10) + hass.states.async_set("sensor.heartworm_low_57", 10) + hass.states.async_set("sensor.heartworm_avg_64", 10) + hass.states.async_set("sensor.heartworm_avg_57", 10) + + value_template_str = """{% if (states.sensor.heartworm_high_80.state|int >= 10) and (states.sensor.heartworm_low_57.state|int >= 10) %} + extreme + {% elif (states.sensor.heartworm_avg_64.state|int >= 30) %} + high + {% elif (states.sensor.heartworm_avg_64.state|int >= 14) %} + moderate + {% elif (states.sensor.heartworm_avg_64.state|int >= 5) %} + slight + {% elif (states.sensor.heartworm_avg_57.state|int >= 5) %} + marginal + {% elif (states.sensor.heartworm_avg_57.state|int < 5) %} + none + {% endif %}""" + + icon_template_str = """{% if is_state('sensor.heartworm_risk',"extreme") %} + mdi:hazard-lights + {% elif is_state('sensor.heartworm_risk',"high") %} + mdi:triangle-outline + {% elif is_state('sensor.heartworm_risk',"moderate") %} + mdi:alert-circle-outline + {% elif is_state('sensor.heartworm_risk',"slight") %} + mdi:exclamation + {% elif is_state('sensor.heartworm_risk',"marginal") %} + mdi:heart + {% elif is_state('sensor.heartworm_risk',"none") %} + mdi:snowflake + {% endif %}""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "heartworm_risk": { + "value_template": value_template_str, + "icon_template": icon_template_str, + }, + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 5 + + hass.states.async_set("sensor.heartworm_high_80", 10) + + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert "Template loop detected" not in caplog.text + + state = hass.states.get("sensor.heartworm_risk") + assert state.state == "extreme" + assert state.attributes[ATTR_ICON] == "mdi:hazard-lights" + + await hass.async_block_till_done() + assert state.state == "extreme" + assert state.attributes[ATTR_ICON] == "mdi:hazard-lights" + assert "Template loop detected" not in caplog.text From 5697f4b4e7f08271554cd34a50813adc27e18983 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 12 Sep 2020 02:07:45 +0800 Subject: [PATCH 804/862] Set output timescale to input timescale (#39946) --- homeassistant/components/stream/worker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 0e861c5cefc..b76896b815a 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -25,7 +25,10 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence): segment, mode="w", format=stream_output.format, - container_options=container_options, + container_options={ + "video_track_timescale": str(int(1 / video_stream.time_base)), + **container_options, + }, ) vstream = output.add_stream(template=video_stream) # Check if audio is requested From b6f868f6295e705108d4a7d7dd6f80470a197f3a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 12 Sep 2020 10:35:51 +0200 Subject: [PATCH 805/862] Add children media class to children spotify media browser (#39953) --- .../components/spotify/media_player.py | 80 ++++++++++++++----- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 7beea59a7bd..0782cb2f390 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -105,24 +105,57 @@ LIBRARY_MAP = { } CONTENT_TYPE_MEDIA_CLASS = { - "current_user_playlists": MEDIA_CLASS_DIRECTORY, - "current_user_followed_artists": MEDIA_CLASS_DIRECTORY, - "current_user_saved_albums": MEDIA_CLASS_DIRECTORY, - "current_user_saved_tracks": MEDIA_CLASS_DIRECTORY, - "current_user_saved_shows": MEDIA_CLASS_DIRECTORY, - "current_user_recently_played": MEDIA_CLASS_DIRECTORY, - "current_user_top_artists": MEDIA_CLASS_DIRECTORY, - "current_user_top_tracks": MEDIA_CLASS_DIRECTORY, - "featured_playlists": MEDIA_CLASS_DIRECTORY, - "categories": MEDIA_CLASS_DIRECTORY, - "category_playlists": MEDIA_CLASS_DIRECTORY, - "new_releases": MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, - MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, - MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, - MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, - MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST, - MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK, + "current_user_playlists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PLAYLIST, + }, + "current_user_followed_artists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ARTIST, + }, + "current_user_saved_albums": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ALBUM, + }, + "current_user_saved_tracks": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_TRACK, + }, + "current_user_saved_shows": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PODCAST, + }, + "current_user_recently_played": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_TRACK, + }, + "current_user_top_artists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_ARTIST, + }, + "current_user_top_tracks": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_TRACK, + }, + "featured_playlists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PLAYLIST, + }, + "categories": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE}, + "category_playlists": { + "parent": MEDIA_CLASS_DIRECTORY, + "children": MEDIA_CLASS_PLAYLIST, + }, + "new_releases": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM}, + MEDIA_TYPE_PLAYLIST: { + "parent": MEDIA_CLASS_PLAYLIST, + "children": MEDIA_CLASS_TRACK, + }, + MEDIA_TYPE_ALBUM: {"parent": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK}, + MEDIA_TYPE_ARTIST: {"parent": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM}, + MEDIA_TYPE_EPISODE: {"parent": MEDIA_CLASS_EPISODE, "children": None}, + MEDIA_TYPE_SHOW: {"parent": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_EPISODE}, + MEDIA_TYPE_TRACK: {"parent": MEDIA_CLASS_TRACK, "children": None}, } @@ -543,7 +576,8 @@ def build_item_response(spotify, user, payload): if media_content_type == "categories": media_item = BrowseMedia( title=LIBRARY_MAP.get(media_content_id), - media_class=media_class, + media_class=media_class["parent"], + children_media_class=media_class["children"], media_content_id=media_content_id, media_content_type=media_content_type, can_play=False, @@ -560,6 +594,7 @@ def build_item_response(spotify, user, payload): BrowseMedia( title=item.get("name"), media_class=MEDIA_CLASS_PLAYLIST, + children_media_class=MEDIA_CLASS_TRACK, media_content_id=item_id, media_content_type="category_playlists", thumbnail=fetch_image_url(item, key="icons"), @@ -567,7 +602,6 @@ def build_item_response(spotify, user, payload): can_expand=True, ) ) - media_item.children_media_class = MEDIA_CLASS_GENRE return media_item if title is None: @@ -578,7 +612,8 @@ def build_item_response(spotify, user, payload): params = { "title": title, - "media_class": media_class, + "media_class": media_class["parent"], + "children_media_class": media_class["children"], "media_content_id": media_content_id, "media_content_type": media_content_type, "can_play": media_content_type in PLAYABLE_MEDIA_TYPES, @@ -625,7 +660,8 @@ def item_payload(item): payload = { "title": item.get("name"), - "media_class": media_class, + "media_class": media_class["parent"], + "children_media_class": media_class["children"], "media_content_id": media_id, "media_content_type": media_type, "can_play": media_type in PLAYABLE_MEDIA_TYPES, From 172a02a605f090bc5b6498dfa9b801d12996cb69 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Sep 2020 12:28:57 +0000 Subject: [PATCH 806/862] Bumped version to 0.115.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2eb1d3c6d83..3d37720a718 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b6" +PATCH_VERSION = "0b7" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From d0e6b3e2683614d16a2f78761df64cec7d72f768 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 13 Sep 2020 03:19:37 +0800 Subject: [PATCH 807/862] Remove skip_sidx container option in stream (#39970) * Remove skip_sidx container option * Add comment --- homeassistant/components/stream/hls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 1e97ac222ec..816d1231c4c 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -148,7 +148,8 @@ class HlsStreamOutput(StreamOutput): def container_options(self) -> Callable[[int], dict]: """Return Callable which takes a sequence number and returns container options.""" return lambda sequence: { - "movflags": "frag_custom+empty_moov+default_base_moof+skip_sidx+frag_discont", + # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 + "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont", "avoid_negative_ts": "make_non_negative", "fragment_index": str(sequence), } From bc2173747c17ea43f741b41cc83a8579cc78dbd1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 12 Sep 2020 08:54:00 -0500 Subject: [PATCH 808/862] Fix children_media_class for special folders (#39974) --- homeassistant/components/plex/media_browser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 28444c4a351..3c91362deb9 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -205,6 +205,7 @@ def special_library_payload(parent_payload, special_type): media_content_type=parent_payload.media_content_type, can_play=False, can_expand=True, + children_media_class=parent_payload.children_media_class, ) From da6885af6c1e258036203f3dde6fcaa0441ad638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 12 Sep 2020 23:18:48 +0200 Subject: [PATCH 809/862] Bump frontend to 20200912.0 (#39997) --- 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 7ccb606894a..d5b48a9b185 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200909.0"], + "requirements": ["home-assistant-frontend==20200912.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index afbec0dd3d0..98c6a02efa2 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.37.0 -home-assistant-frontend==20200909.0 +home-assistant-frontend==20200912.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 4c76d1c2d2c..7a2f5147e14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200909.0 +home-assistant-frontend==20200912.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f07db06eff0..deae7ed6caa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200909.0 +home-assistant-frontend==20200912.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From b9b76b351983558a3c284b65369e5ed58a667375 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 12 Sep 2020 14:53:41 -0700 Subject: [PATCH 810/862] Bump androidtv to 0.0.50 (#39998) --- homeassistant/components/androidtv/manifest.json | 2 +- homeassistant/components/androidtv/media_player.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 8e62813714e..f6c773941f1 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.2.1", - "androidtv[async]==0.0.49", + "androidtv[async]==0.0.50", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 959c85abd77..1ea20dbeca5 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -380,7 +380,7 @@ def adb_decorator(override_available=False): # An unforeseen exception occurred. Close the ADB connection so that # it doesn't happen over and over again, then raise the exception. await self.aftv.adb_close() - self._available = False # pylint: disable=protected-access + self._available = False raise return _adb_exception_catcher diff --git a/requirements_all.txt b/requirements_all.txt index 7a2f5147e14..a01bd361ef0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -248,7 +248,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.49 +androidtv[async]==0.0.50 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index deae7ed6caa..bc875b264cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,7 +149,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.49 +androidtv[async]==0.0.50 # homeassistant.components.apns apns2==0.3.0 From 951c3731102374f243df0327cbce937bf8171a7b Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 13 Sep 2020 01:13:57 +0200 Subject: [PATCH 811/862] Fix Freebox call sensor when no call in history (#40001) --- homeassistant/components/freebox/sensor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index dc0d808c438..aeeaba438ff 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -146,11 +146,12 @@ class FreeboxCallSensor(FreeboxSensor): def async_update_state(self) -> None: """Update the Freebox call sensor.""" self._call_list_for_type = [] - for call in self._router.call_list: - if not call["new"]: - continue - if call["type"] == self._sensor_type: - self._call_list_for_type.append(call) + if self._router.call_list: + for call in self._router.call_list: + if not call["new"]: + continue + if call["type"] == self._sensor_type: + self._call_list_for_type.append(call) self._state = len(self._call_list_for_type) From 47326b2295a628a99c56f4ad7c1e10c72e223b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 13 Sep 2020 11:30:51 +0200 Subject: [PATCH 812/862] Bump pyhaversion to 3.4.0 (#40016) --- CODEOWNERS | 2 +- homeassistant/components/version/manifest.json | 4 ++-- homeassistant/components/version/sensor.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 42c39544540..75714f1aede 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -466,7 +466,7 @@ homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 homeassistant/components/vera/* @vangorra homeassistant/components/versasense/* @flamm3blemuff1n -homeassistant/components/version/* @fabaff +homeassistant/components/version/* @fabaff @ludeeus homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey homeassistant/components/vicare/* @oischinger homeassistant/components/vilfo/* @ManneW diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index ed3158040d5..1f07c757ad8 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,7 +2,7 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==3.3.0"], - "codeowners": ["@fabaff"], + "requirements": ["pyhaversion==3.4.0"], + "codeowners": ["@fabaff", "@ludeeus"], "quality_scale": "internal" } diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 636e564b816..dcfdc115376 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -35,6 +35,7 @@ ALL_IMAGES = [ "raspberrypi4-64", "tinker", "odroid-c2", + "odroid-n2", "odroid-xu", ] ALL_SOURCES = ["local", "pypi", "hassio", "docker", "haio"] diff --git a/requirements_all.txt b/requirements_all.txt index a01bd361ef0..0faab26721f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1377,7 +1377,7 @@ pygtfs==0.1.5 pygti==0.6.0 # homeassistant.components.version -pyhaversion==3.3.0 +pyhaversion==3.4.0 # homeassistant.components.heos pyheos==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc875b264cb..5519f7f3858 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -662,7 +662,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.6.0 # homeassistant.components.version -pyhaversion==3.3.0 +pyhaversion==3.4.0 # homeassistant.components.heos pyheos==0.6.0 From b19fe17e76d5fcfe9a6861d21bddae690c333c6d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 13 Sep 2020 11:41:46 +0200 Subject: [PATCH 813/862] Bumped version to 0.115.0b8 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3d37720a718..a1df155f1e7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b7" +PATCH_VERSION = "0b8" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 5a6492b76de269a94a7863c132ba39f3c12b282c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 13 Sep 2020 15:38:31 +0200 Subject: [PATCH 814/862] Update azure-pipelines-wheels.yml --- azure-pipelines-wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index ac5f4fd824f..bcf16e8dee7 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -47,7 +47,7 @@ jobs: - template: templates/azp-job-wheels.yaml@azure parameters: builderVersion: '$(versionWheels)' - builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' + builderApk: 'build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' builderPip: 'Cython;numpy;scikit-build' builderEnvFile: true skipBinary: 'aiohttp' @@ -94,7 +94,7 @@ jobs: # Write env for build settings ( - echo "GRPC_BUILD_WITH_BORING_SSL_ASM=0" + echo "GRPC_BUILD_WITH_BORING_SSL_ASM=" echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1" ) > .env_file displayName: 'Prepare requirements files for Home Assistant wheels' From 30ef7a5e88bb7cb3caa2612ed160b0b13b19a25d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 09:12:10 -0500 Subject: [PATCH 815/862] Suppress homekit bridge discovery by homekit controller (#39990) --- .../homekit_controller/config_flow.py | 28 ++++++++++++- .../homekit_controller/strings.json | 1 + .../homekit_controller/test_config_flow.py | 39 ++++++++++++++++++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 2c69512db9d..7e98fc40910 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -8,12 +8,19 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.core import callback +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_get_registry as async_get_device_registry, +) from .connection import get_accessory_name, get_bridge_information from .const import DOMAIN, KNOWN_DEVICES -HOMEKIT_IGNORE = ["Home Assistant Bridge"] HOMEKIT_DIR = ".homekit" +HOMEKIT_BRIDGE_DOMAIN = "homekit" +HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge" +HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge" + PAIRING_FILE = "pairing.json" PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") @@ -141,6 +148,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): return self.async_abort(reason="no_devices") + async def _hkid_is_homekit_bridge(self, hkid): + """Determine if the device is a homekit bridge.""" + dev_reg = await async_get_device_registry(self.hass) + device = dev_reg.async_get_device( + identifiers=set(), connections={(CONNECTION_NETWORK_MAC, hkid)} + ) + + if device is None: + return False + return device.model == HOMEKIT_BRIDGE_MODEL + async def async_step_zeroconf(self, discovery_info): """Handle a discovered HomeKit accessory. @@ -153,6 +171,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): key.lower(): value for (key, value) in discovery_info["properties"].items() } + if "id" not in properties: + _LOGGER.warning( + "HomeKit device %s: id not exposed, in violation of spec", properties + ) + return self.async_abort(reason="invalid_properties") + # The hkid is a unique random number that looks like a pairing code. # It changes if a device is factory reset. hkid = properties["id"] @@ -208,7 +232,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # Devices in HOMEKIT_IGNORE have native local integrations - users # should be encouraged to use native integration and not confused # by alternative HK API. - if model in HOMEKIT_IGNORE: + if await self._hkid_is_homekit_bridge(hkid): return self.async_abort(reason="ignored_model") self.model = model diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index bc07b71fa75..e685a46e144 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -44,6 +44,7 @@ "already_configured": "Accessory is already configured with this controller.", "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", + "invalid_properties": "Invalid properties announced by device.", "already_in_progress": "Config flow for device is already in progress." } } diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 09c823ff498..e5c8e381a5f 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -8,10 +8,11 @@ from aiohomekit.model.services import ServicesTypes import pytest from homeassistant.components.homekit_controller import config_flow +from homeassistant.helpers import device_registry import tests.async_mock from tests.async_mock import patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_device_registry PAIRING_START_FORM_ERRORS = [ (KeyError, "pairing_failed"), @@ -233,11 +234,45 @@ async def test_pair_already_paired_1(hass, controller): assert result["reason"] == "already_paired" +async def test_id_missing(hass, controller): + """Test id is missing.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + + # Remove id from device + del discovery_info["properties"]["id"] + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + assert result["type"] == "abort" + assert result["reason"] == "invalid_properties" + + async def test_discovery_ignored_model(hass, controller): """Already paired.""" device = setup_mock_accessory(controller) discovery_info = get_device_discovery_info(device) - discovery_info["properties"]["md"] = config_flow.HOMEKIT_IGNORE[0] + + config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={}) + formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF") + + dev_reg = mock_device_registry(hass) + dev_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={ + ( + config_flow.HOMEKIT_BRIDGE_DOMAIN, + config_entry.entry_id, + config_flow.HOMEKIT_BRIDGE_SERIAL_NUMBER, + ) + }, + connections={(device_registry.CONNECTION_NETWORK_MAC, formatted_mac)}, + model=config_flow.HOMEKIT_BRIDGE_MODEL, + ) + + discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF" # Device is discovered result = await hass.config_entries.flow.async_init( From b81c61dd99bfc7f0c28855f7190dac44b95ff5c5 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Sep 2020 22:04:48 +0200 Subject: [PATCH 816/862] Fix requiring username or password for nzbget yaml config (#40003) --- homeassistant/components/nzbget/config_flow.py | 4 ++-- homeassistant/components/nzbget/coordinator.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index dfed7a9bfee..f593eeb0729 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -38,8 +38,8 @@ def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: """ nzbget_api = NZBGetAPI( data[CONF_HOST], - data[CONF_USERNAME] if data[CONF_USERNAME] != "" else None, - data[CONF_PASSWORD] if data[CONF_PASSWORD] != "" else None, + data.get(CONF_USERNAME), + data.get(CONF_PASSWORD), data[CONF_SSL], data[CONF_VERIFY_SSL], data[CONF_PORT], diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index 8892475bc09..9a76d802bdd 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -29,8 +29,8 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): """Initialize global NZBGet data updater.""" self.nzbget = NZBGetAPI( config[CONF_HOST], - config[CONF_USERNAME] if config[CONF_USERNAME] != "" else None, - config[CONF_PASSWORD] if config[CONF_PASSWORD] != "" else None, + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), config[CONF_SSL], config[CONF_VERIFY_SSL], config[CONF_PORT], From f0ce65af7da57af38388685702c060b35149f319 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 8 Sep 2020 08:37:44 +0200 Subject: [PATCH 817/862] Add tests for Plugwise integration (#36371) --- .coveragerc | 5 - .../components/plugwise/config_flow.py | 1 - requirements_test.txt | 1 + tests/components/plugwise/common.py | 26 +++ tests/components/plugwise/conftest.py | 167 ++++++++++++++++++ .../components/plugwise/test_binary_sensor.py | 37 ++++ tests/components/plugwise/test_climate.py | 161 +++++++++++++++++ tests/components/plugwise/test_config_flow.py | 5 +- tests/components/plugwise/test_init.py | 45 +++++ tests/components/plugwise/test_sensor.py | 66 +++++++ tests/components/plugwise/test_switch.py | 50 ++++++ .../get_all_devices.json | 1 + .../02cf28bfec924855854c544690a609ef.json | 1 + .../21f2b542c49845e6bb416884c55778d6.json | 1 + .../4a810418d5394b3f82727340b91ba740.json | 1 + .../675416a629f343c495449970e2ca37b5.json | 1 + .../680423ff840043738f42cc7f1ff97a36.json | 1 + .../6a3bf693d05e48e0b460c815a4fdd09d.json | 1 + .../78d1126fc4c743db81b61c20e88342a7.json | 1 + .../90986d591dcd426cae3ec3e8111ff730.json | 1 + .../a28f588dc4a049a483fd03a30361ad3a.json | 1 + .../a2c3583e0a6349358998b760cea82d2a.json | 1 + .../b310b72a0e354bfab43089919b9a88bf.json | 1 + .../b59bcebaf94b499ea7d46e4a66fb62d8.json | 1 + .../cd0ddb54ef694e11ac18ed1cbce5dbbd.json | 1 + .../d3da73bde12a47d5a6b8f9dad971f2ec.json | 1 + .../df4a4a8169904cdb9c03d61a21f42140.json | 1 + .../e7693eb9582644e5b865dba8d4447cf1.json | 1 + .../f1fee6043d3642a9b0a65297455f008e.json | 1 + .../fe799307f1624099878210aa0b9f1475.json | 1 + .../anna_heatpump/get_all_devices.json | 1 + .../015ae9ea3f964e668e490fa39da3870b.json | 1 + .../1cbf783bb11e4a7c8a6843dee3a86927.json | 1 + .../3cb70739631c4d17a86b8b12e8a5161b.json | 1 + .../p1v3_full_option/get_all_devices.json | 1 + .../e950c7d5e1ee407a858e2a8b5016c8b3.json | 1 + 36 files changed, 580 insertions(+), 9 deletions(-) create mode 100644 tests/components/plugwise/common.py create mode 100644 tests/components/plugwise/conftest.py create mode 100644 tests/components/plugwise/test_binary_sensor.py create mode 100644 tests/components/plugwise/test_climate.py create mode 100644 tests/components/plugwise/test_init.py create mode 100644 tests/components/plugwise/test_sensor.py create mode 100644 tests/components/plugwise/test_switch.py create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json create mode 100644 tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json create mode 100644 tests/fixtures/plugwise/anna_heatpump/get_all_devices.json create mode 100644 tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json create mode 100644 tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json create mode 100644 tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json create mode 100644 tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json create mode 100644 tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json diff --git a/.coveragerc b/.coveragerc index a86b6312a77..162e0c65f06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -656,11 +656,6 @@ omit = homeassistant/components/plaato/* homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py - homeassistant/components/plugwise/__init__.py - homeassistant/components/plugwise/binary_sensor.py - homeassistant/components/plugwise/climate.py - homeassistant/components/plugwise/sensor.py - homeassistant/components/plugwise/switch.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 4d1752a2774..20c8a5a216c 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -106,7 +106,6 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" - if not errors: await self.async_set_unique_id(api.gateway_id) self._abort_if_unique_id_configured() diff --git a/requirements_test.txt b/requirements_test.txt index 86b8b496e83..e36837edf63 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,6 +7,7 @@ asynctest==0.13.0 codecov==2.1.0 coverage==5.2.1 +jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.780 pre-commit==2.7.1 diff --git a/tests/components/plugwise/common.py b/tests/components/plugwise/common.py new file mode 100644 index 00000000000..eb227322aa8 --- /dev/null +++ b/tests/components/plugwise/common.py @@ -0,0 +1,26 @@ +"""Common initialisation for the Plugwise integration.""" + +from homeassistant.components.plugwise import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def async_init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + skip_setup: bool = False, +): + """Initialize the Smile integration.""" + + entry = MockConfigEntry( + domain=DOMAIN, data={"host": "1.1.1.1", "password": "test-password"} + ) + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py new file mode 100644 index 00000000000..8564b2c0d8c --- /dev/null +++ b/tests/components/plugwise/conftest.py @@ -0,0 +1,167 @@ +"""Setup mocks for the Plugwise integration tests.""" + +from functools import partial +import re + +from Plugwise_Smile.Smile import Smile +import jsonpickle +import pytest + +from tests.async_mock import AsyncMock, patch +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +def _read_json(environment, call): + """Undecode the json data.""" + fixture = load_fixture(f"plugwise/{environment}/{call}.json") + return jsonpickle.decode(fixture) + + +@pytest.fixture(name="mock_smile") +def mock_smile(): + """Create a Mock Smile for testing exceptions.""" + with patch( + "homeassistant.components.plugwise.config_flow.Smile", + ) as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.return_value.connect.return_value = True + yield smile_mock.return_value + + +@pytest.fixture(name="mock_smile_unauth") +def mock_smile_unauth(aioclient_mock: AiohttpClientMocker) -> None: + """Mock the Plugwise Smile unauthorized for Home Assistant.""" + aioclient_mock.get(re.compile(".*"), status=401) + aioclient_mock.put(re.compile(".*"), status=401) + + +@pytest.fixture(name="mock_smile_error") +def mock_smile_error(aioclient_mock: AiohttpClientMocker) -> None: + """Mock the Plugwise Smile server failure for Home Assistant.""" + aioclient_mock.get(re.compile(".*"), status=500) + aioclient_mock.put(re.compile(".*"), status=500) + + +@pytest.fixture(name="mock_smile_notconnect") +def mock_smile_notconnect(): + """Mock the Plugwise Smile general connection failure for Home Assistant.""" + with patch("homeassistant.components.plugwise.Smile") as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.PlugwiseError = Smile.PlugwiseError + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=False) + yield smile_mock.return_value + + +def _get_device_data(chosen_env, device_id): + """Mock return data for specific devices.""" + return _read_json(chosen_env, "get_device_data/" + device_id) + + +@pytest.fixture(name="mock_smile_adam") +def mock_smile_adam(): + """Create a Mock Adam environment for testing exceptions.""" + chosen_env = "adam_multiple_devices_per_zone" + with patch("homeassistant.components.plugwise.Smile") as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + + smile_mock.return_value.gateway_id = "fe799307f1624099878210aa0b9f1475" + smile_mock.return_value.heater_id = "90986d591dcd426cae3ec3e8111ff730" + smile_mock.return_value.smile_version = "3.0.15" + smile_mock.return_value.smile_type = "thermostat" + + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.full_update_device.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_schedule_state.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_preset.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.set_temperature.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_relay_state.side_effect = AsyncMock( + return_value=True + ) + + smile_mock.return_value.get_all_devices.return_value = _read_json( + chosen_env, "get_all_devices" + ) + smile_mock.return_value.get_device_data.side_effect = partial( + _get_device_data, chosen_env + ) + + yield smile_mock.return_value + + +@pytest.fixture(name="mock_smile_anna") +def mock_smile_anna(): + """Create a Mock Anna environment for testing exceptions.""" + chosen_env = "anna_heatpump" + with patch("homeassistant.components.plugwise.Smile") as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + + smile_mock.return_value.gateway_id = "015ae9ea3f964e668e490fa39da3870b" + smile_mock.return_value.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" + smile_mock.return_value.smile_version = "4.0.15" + smile_mock.return_value.smile_type = "thermostat" + + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.full_update_device.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_schedule_state.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_preset.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.set_temperature.side_effect = AsyncMock( + return_value=True + ) + smile_mock.return_value.set_relay_state.side_effect = AsyncMock( + return_value=True + ) + + smile_mock.return_value.get_all_devices.return_value = _read_json( + chosen_env, "get_all_devices" + ) + smile_mock.return_value.get_device_data.side_effect = partial( + _get_device_data, chosen_env + ) + + yield smile_mock.return_value + + +@pytest.fixture(name="mock_smile_p1") +def mock_smile_p1(): + """Create a Mock P1 DSMR environment for testing exceptions.""" + chosen_env = "p1v3_full_option" + with patch("homeassistant.components.plugwise.Smile") as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.XMLDataMissingError = Smile.XMLDataMissingError + + smile_mock.return_value.gateway_id = "e950c7d5e1ee407a858e2a8b5016c8b3" + smile_mock.return_value.heater_id = None + smile_mock.return_value.smile_version = "3.3.9" + smile_mock.return_value.smile_type = "power" + + smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) + smile_mock.return_value.full_update_device.side_effect = AsyncMock( + return_value=True + ) + + smile_mock.return_value.get_all_devices.return_value = _read_json( + chosen_env, "get_all_devices" + ) + smile_mock.return_value.get_device_data.side_effect = partial( + _get_device_data, chosen_env + ) + + yield smile_mock.return_value diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py new file mode 100644 index 00000000000..b2221194d8e --- /dev/null +++ b/tests/components/plugwise/test_binary_sensor.py @@ -0,0 +1,37 @@ +"""Tests for the Plugwise binary_sensor integration.""" + +from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.const import STATE_OFF, STATE_ON + +from tests.components.plugwise.common import async_init_integration + + +async def test_anna_climate_binary_sensor_entities(hass, mock_smile_anna): + """Test creation of climate related binary_sensor entities.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("binary_sensor.auxiliary_slave_boiler_state") + assert str(state.state) == STATE_OFF + + state = hass.states.get("binary_sensor.auxiliary_dhw_state") + assert str(state.state) == STATE_OFF + + +async def test_anna_climate_binary_sensor_change(hass, mock_smile_anna): + """Test change of climate related binary_sensor entities.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + hass.states.async_set("binary_sensor.auxiliary_dhw_state", STATE_ON, {}) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.auxiliary_dhw_state") + assert str(state.state) == STATE_ON + + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.auxiliary_dhw_state" + ) + + state = hass.states.get("binary_sensor.auxiliary_dhw_state") + assert str(state.state) == STATE_OFF diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py new file mode 100644 index 00000000000..02375b49709 --- /dev/null +++ b/tests/components/plugwise/test_climate.py @@ -0,0 +1,161 @@ +"""Tests for the Plugwise Climate integration.""" + +from homeassistant.config_entries import ENTRY_STATE_LOADED + +from tests.components.plugwise.common import async_init_integration + + +async def test_adam_climate_entity_attributes(hass, mock_smile_adam): + """Test creation of adam climate device environment.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("climate.zone_lisa_wk") + attrs = state.attributes + + assert attrs["hvac_modes"] is None + + assert "preset_modes" in attrs + assert "no_frost" in attrs["preset_modes"] + assert "home" in attrs["preset_modes"] + + assert attrs["current_temperature"] == 20.9 + assert attrs["temperature"] == 21.5 + + assert attrs["preset_mode"] == "home" + + assert attrs["supported_features"] == 17 + + state = hass.states.get("climate.zone_thermostat_jessie") + attrs = state.attributes + + assert attrs["hvac_modes"] is None + + assert "preset_modes" in attrs + assert "no_frost" in attrs["preset_modes"] + assert "home" in attrs["preset_modes"] + + assert attrs["current_temperature"] == 17.2 + assert attrs["temperature"] == 15.0 + + assert attrs["preset_mode"] == "asleep" + + +async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): + """Test handling of user requests in adam climate device environment.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.zone_lisa_wk", "temperature": 25}, + blocking=True, + ) + state = hass.states.get("climate.zone_lisa_wk") + attrs = state.attributes + + assert attrs["temperature"] == 25.0 + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": "climate.zone_lisa_wk", "preset_mode": "away"}, + blocking=True, + ) + state = hass.states.get("climate.zone_lisa_wk") + attrs = state.attributes + + assert attrs["preset_mode"] == "away" + + assert attrs["supported_features"] == 17 + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.zone_thermostat_jessie", "temperature": 25}, + blocking=True, + ) + + state = hass.states.get("climate.zone_thermostat_jessie") + attrs = state.attributes + + assert attrs["temperature"] == 25.0 + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": "climate.zone_thermostat_jessie", "preset_mode": "home"}, + blocking=True, + ) + state = hass.states.get("climate.zone_thermostat_jessie") + attrs = state.attributes + + assert attrs["preset_mode"] == "home" + + +async def test_anna_climate_entity_attributes(hass, mock_smile_anna): + """Test creation of anna climate device environment.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("climate.anna") + attrs = state.attributes + + assert "hvac_modes" in attrs + assert "heat_cool" in attrs["hvac_modes"] + + assert "preset_modes" in attrs + assert "no_frost" in attrs["preset_modes"] + assert "home" in attrs["preset_modes"] + + assert attrs["current_temperature"] == 23.3 + assert attrs["temperature"] == 21.0 + + assert state.state == "auto" + assert attrs["hvac_action"] == "idle" + assert attrs["preset_mode"] == "home" + + assert attrs["supported_features"] == 17 + + +async def test_anna_climate_entity_climate_changes(hass, mock_smile_anna): + """Test handling of user requests in anna climate device environment.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.anna", "temperature": 25}, + blocking=True, + ) + + state = hass.states.get("climate.anna") + attrs = state.attributes + + assert attrs["temperature"] == 25.0 + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": "climate.anna", "preset_mode": "away"}, + blocking=True, + ) + + state = hass.states.get("climate.anna") + attrs = state.attributes + + assert attrs["preset_mode"] == "away" + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": "climate.anna", "hvac_mode": "heat_cool"}, + blocking=True, + ) + + state = hass.states.get("climate.anna") + attrs = state.attributes + + assert state.state == "heat_cool" diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 11c0000977a..219ba8aee7f 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -62,14 +62,14 @@ async def test_form(hass): {"host": TEST_HOST, "password": TEST_PASSWORD}, ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["data"] == { "host": TEST_HOST, "password": TEST_PASSWORD, } - await hass.async_block_till_done() - assert result["errors"] == {} assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -108,7 +108,6 @@ async def test_zeroconf_form(hass): "password": TEST_PASSWORD, } - assert result["errors"] == {} assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py new file mode 100644 index 00000000000..713cd4930d7 --- /dev/null +++ b/tests/components/plugwise/test_init.py @@ -0,0 +1,45 @@ +"""Tests for the Plugwise Climate integration.""" + +import asyncio + +from Plugwise_Smile.Smile import Smile + +from homeassistant.config_entries import ( + ENTRY_STATE_SETUP_ERROR, + ENTRY_STATE_SETUP_RETRY, +) + +from tests.components.plugwise.common import async_init_integration + + +async def test_smile_unauthorized(hass, mock_smile_unauth): + """Test failing unauthorization by Smile.""" + entry = await async_init_integration(hass, mock_smile_unauth) + assert entry.state == ENTRY_STATE_SETUP_ERROR + + +async def test_smile_error(hass, mock_smile_error): + """Test server error handling by Smile.""" + entry = await async_init_integration(hass, mock_smile_error) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_smile_notconnect(hass, mock_smile_notconnect): + """Connection failure error handling by Smile.""" + mock_smile_notconnect.connect.return_value = False + entry = await async_init_integration(hass, mock_smile_notconnect) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_smile_timeout(hass, mock_smile_notconnect): + """Timeout error handling by Smile.""" + mock_smile_notconnect.connect.side_effect = asyncio.TimeoutError + entry = await async_init_integration(hass, mock_smile_notconnect) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_smile_adam_xmlerror(hass, mock_smile_adam): + """Detect malformed XML by Smile in Adam environment.""" + mock_smile_adam.full_update_device.side_effect = Smile.XMLDataMissingError + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py new file mode 100644 index 00000000000..bc586120517 --- /dev/null +++ b/tests/components/plugwise/test_sensor.py @@ -0,0 +1,66 @@ +"""Tests for the Plugwise Sensor integration.""" + +from homeassistant.config_entries import ENTRY_STATE_LOADED + +from tests.components.plugwise.common import async_init_integration + + +async def test_adam_climate_sensor_entities(hass, mock_smile_adam): + """Test creation of climate related sensor entities.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("sensor.adam_outdoor_temperature") + assert float(state.state) == 7.81 + + state = hass.states.get("sensor.cv_pomp_electricity_consumed") + assert float(state.state) == 35.6 + + state = hass.states.get("sensor.auxiliary_water_temperature") + assert float(state.state) == 70.0 + + state = hass.states.get("sensor.cv_pomp_electricity_consumed_interval") + assert float(state.state) == 7.37 + + await hass.helpers.entity_component.async_update_entity( + "sensor.zone_lisa_wk_battery" + ) + + state = hass.states.get("sensor.zone_lisa_wk_battery") + assert float(state.state) == 34 + + +async def test_anna_climate_sensor_entities(hass, mock_smile_anna): + """Test creation of climate related sensor entities.""" + entry = await async_init_integration(hass, mock_smile_anna) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("sensor.auxiliary_outdoor_temperature") + assert float(state.state) == 18.0 + + state = hass.states.get("sensor.auxiliary_water_temperature") + assert float(state.state) == 29.1 + + state = hass.states.get("sensor.anna_illuminance") + assert float(state.state) == 86.0 + + +async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): + """Test creation of power related sensor entities.""" + entry = await async_init_integration(hass, mock_smile_p1) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("sensor.p1_net_electricity_point") + assert float(state.state) == -2761.0 + + state = hass.states.get("sensor.p1_electricity_consumed_off_peak_cumulative") + assert int(state.state) == 551 + + state = hass.states.get("sensor.p1_electricity_produced_peak_point") + assert float(state.state) == 2761.0 + + state = hass.states.get("sensor.p1_electricity_consumed_peak_cumulative") + assert int(state.state) == 442 + + state = hass.states.get("sensor.p1_gas_consumed_cumulative") + assert float(state.state) == 584.9 diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py new file mode 100644 index 00000000000..a58ebf83caa --- /dev/null +++ b/tests/components/plugwise/test_switch.py @@ -0,0 +1,50 @@ +"""Tests for the Plugwise switch integration.""" + +from homeassistant.config_entries import ENTRY_STATE_LOADED + +from tests.components.plugwise.common import async_init_integration + + +async def test_adam_climate_switch_entities(hass, mock_smile_adam): + """Test creation of climate related switch entities.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + state = hass.states.get("switch.cv_pomp") + assert str(state.state) == "on" + + state = hass.states.get("switch.fibaro_hc2") + assert str(state.state) == "on" + + +async def test_adam_climate_switch_changes(hass, mock_smile_adam): + """Test changing of climate related switch entities.""" + entry = await async_init_integration(hass, mock_smile_adam) + assert entry.state == ENTRY_STATE_LOADED + + await hass.services.async_call( + "switch", + "turn_off", + {"entity_id": "switch.cv_pomp"}, + blocking=True, + ) + state = hass.states.get("switch.cv_pomp") + assert str(state.state) == "off" + + await hass.services.async_call( + "switch", + "toggle", + {"entity_id": "switch.fibaro_hc2"}, + blocking=True, + ) + state = hass.states.get("switch.fibaro_hc2") + assert str(state.state) == "off" + + await hass.services.async_call( + "switch", + "toggle", + {"entity_id": "switch.fibaro_hc2"}, + blocking=True, + ) + state = hass.states.get("switch.fibaro_hc2") + assert str(state.state) == "on" diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json new file mode 100644 index 00000000000..61ebc4d9a6c --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json @@ -0,0 +1 @@ +{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "types": {"py/set": ["thermostat"]}, "class": "thermostatic_radiator_valve", "location": "446ac08dd04d4eff8ac57489757b7314"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json new file mode 100644 index 00000000000..238da9d846a --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/02cf28bfec924855854c544690a609ef.json @@ -0,0 +1 @@ +{"electricity_consumed": 34.0, "electricity_consumed_interval": 9.15, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json new file mode 100644 index 00000000000..4fcb40c4cf8 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/21f2b542c49845e6bb416884c55778d6.json @@ -0,0 +1 @@ +{"electricity_consumed": 82.6, "electricity_consumed_interval": 8.6, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json new file mode 100644 index 00000000000..feb6290c9c4 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/4a810418d5394b3f82727340b91ba740.json @@ -0,0 +1 @@ +{"electricity_consumed": 8.5, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json new file mode 100644 index 00000000000..74d15fac374 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/675416a629f343c495449970e2ca37b5.json @@ -0,0 +1 @@ +{"electricity_consumed": 12.2, "electricity_consumed_interval": 2.97, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json new file mode 100644 index 00000000000..75bc62fbad4 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json @@ -0,0 +1 @@ +{"setpoint": 14.0, "temperature": 19.1, "battery": 0.51, "valve_position": 0.0, "temperature_difference": -0.4} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json new file mode 100644 index 00000000000..41333f374e1 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json @@ -0,0 +1 @@ +{"setpoint": 15.0, "temperature": 17.2, "battery": 0.37, "active_preset": "asleep", "presets": {"home": [20.0, 22.0], "no_frost": [10.0, 30.0], "away": [12.0, 25.0], "vacation": [11.0, 28.0], "asleep": [16.0, 24.0]}, "schedule_temperature": 15.0, "available_schedules": ["CV Jessie"], "selected_schedule": "CV Jessie", "last_used": "CV Jessie"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json new file mode 100644 index 00000000000..7a9c3e9be01 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/78d1126fc4c743db81b61c20e88342a7.json @@ -0,0 +1 @@ +{"electricity_consumed": 35.6, "electricity_consumed_interval": 7.37, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json new file mode 100644 index 00000000000..5e481d36b46 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json @@ -0,0 +1 @@ +{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 0.01} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json new file mode 100644 index 00000000000..0aeca4cc18e --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a28f588dc4a049a483fd03a30361ad3a.json @@ -0,0 +1 @@ +{"electricity_consumed": 12.5, "electricity_consumed_interval": 3.8, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json new file mode 100644 index 00000000000..eef83a67a20 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json @@ -0,0 +1 @@ +{"setpoint": 13.0, "temperature": 17.2, "battery": 0.62, "valve_position": 0.0, "temperature_difference": -0.2} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json new file mode 100644 index 00000000000..16da5f44ef5 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json @@ -0,0 +1 @@ +{"setpoint": 21.5, "temperature": 26.0, "valve_position": 1.0, "temperature_difference": 3.5} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json new file mode 100644 index 00000000000..65fa0dd3d52 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json @@ -0,0 +1 @@ +{"setpoint": 21.5, "temperature": 20.9, "battery": 0.34, "active_preset": "home", "presets": {"vacation": [15.0, 28.0], "asleep": [18.0, 24.0], "no_frost": [12.0, 30.0], "away": [17.0, 25.0], "home": [21.5, 22.0]}, "schedule_temperature": 21.5, "available_schedules": ["GF7 Woonkamer"], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json new file mode 100644 index 00000000000..fbefc5bba25 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/cd0ddb54ef694e11ac18ed1cbce5dbbd.json @@ -0,0 +1 @@ +{"electricity_consumed": 16.5, "electricity_consumed_interval": 0.5, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json new file mode 100644 index 00000000000..fd202e05586 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json @@ -0,0 +1 @@ +{"setpoint": 15.0, "temperature": 17.1, "battery": 0.62, "valve_position": 0.0, "temperature_difference": 0.1} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json new file mode 100644 index 00000000000..12947c42ce0 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json @@ -0,0 +1 @@ +{"setpoint": 13.0, "temperature": 16.5, "battery": 0.67, "active_preset": "away", "presets": {"home": [20.0, 22.0], "away": [12.0, 25.0], "vacation": [12.0, 28.0], "no_frost": [8.0, 30.0], "asleep": [15.0, 24.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json new file mode 100644 index 00000000000..151b4b41f70 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json @@ -0,0 +1 @@ +{"setpoint": 5.5, "temperature": 15.6, "battery": 0.68, "valve_position": 0.0, "temperature_difference": 0.0, "active_preset": "no_frost", "presets": {"home": [20.0, 22.0], "asleep": [17.0, 24.0], "away": [15.0, 25.0], "vacation": [15.0, 28.0], "no_frost": [10.0, 30.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json new file mode 100644 index 00000000000..9934e109033 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json @@ -0,0 +1 @@ +{"setpoint": 14.0, "temperature": 18.9, "battery": 0.92, "active_preset": "away", "presets": {"asleep": [17.0, 24.0], "no_frost": [10.0, 30.0], "away": [14.0, 25.0], "home": [21.0, 22.0], "vacation": [12.0, 28.0]}, "schedule_temperature": 14.0, "available_schedules": ["Badkamer Schema"], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json new file mode 100644 index 00000000000..ef325af7bc2 --- /dev/null +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/fe799307f1624099878210aa0b9f1475.json @@ -0,0 +1 @@ +{"outdoor_temperature": 7.81} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json new file mode 100644 index 00000000000..4992a175b14 --- /dev/null +++ b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json @@ -0,0 +1 @@ +{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "types": {"py/set": ["thermostat"]}, "class": "thermostat", "location": "c784ee9fdab44e1395b8dee7d7a497d5"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json new file mode 100644 index 00000000000..750aa8b455c --- /dev/null +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/015ae9ea3f964e668e490fa39da3870b.json @@ -0,0 +1 @@ +{"outdoor_temperature": 20.2} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json new file mode 100644 index 00000000000..a8aea8e1357 --- /dev/null +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json @@ -0,0 +1 @@ +{"outdoor_temperature": 18.0, "heating_state": false, "dhw_state": false, "water_temperature": 29.1, "return_temperature": 25.1, "water_pressure": 1.57, "intended_boiler_temperature": 0.0, "modulation_level": 0.52, "cooling_state": false, "slave_boiler_state": false, "compressor_state": true, "flame_state": false} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json new file mode 100644 index 00000000000..2a092e792d5 --- /dev/null +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json @@ -0,0 +1 @@ +{"setpoint": 21.0, "temperature": 23.3, "active_preset": "home", "presets": {"no_frost": [10.0, 30.0], "home": [21.0, 22.0], "away": [20.0, 25.0], "asleep": [20.5, 24.0], "vacation": [17.0, 28.0]}, "schedule_temperature": null, "available_schedules": ["standaard"], "selected_schedule": "standaard", "last_used": "standaard", "illuminance": 86.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json new file mode 100644 index 00000000000..e25fcb953c8 --- /dev/null +++ b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json @@ -0,0 +1 @@ +{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "types": {"py/set": ["power", "home"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json new file mode 100644 index 00000000000..36cb66c7902 --- /dev/null +++ b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json @@ -0,0 +1 @@ +{"net_electricity_point": -2761.0, "electricity_consumed_peak_point": 0.0, "electricity_consumed_off_peak_point": 0.0, "net_electricity_cumulative": 442972.0, "electricity_consumed_peak_cumulative": 442932.0, "electricity_consumed_off_peak_cumulative": 551090.0, "net_electricity_interval": 0.0, "electricity_consumed_peak_interval": 0.0, "electricity_consumed_off_peak_interval": 0.0, "electricity_produced_peak_point": 2761.0, "electricity_produced_off_peak_point": 0.0, "electricity_produced_peak_cumulative": 396559.0, "electricity_produced_off_peak_cumulative": 154491.0, "electricity_produced_peak_interval": 0.0, "electricity_produced_off_peak_interval": 0.0, "gas_consumed_cumulative": 584.9, "gas_consumed_interval": 0.0} \ No newline at end of file From 8b4e1936146b2455b8140f5ad28deef5f1831cfb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 13 Sep 2020 11:02:49 +0200 Subject: [PATCH 818/862] Ensure Plugwise unique_id is correctly set (#40014) * Ensure unique_id is correctly set * Removed unnec. line Co-authored-by: Tom Scholten --- homeassistant/components/plugwise/__init__.py | 4 +++ .../components/plugwise/config_flow.py | 8 +++++- tests/components/plugwise/conftest.py | 3 ++ tests/components/plugwise/test_config_flow.py | 28 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 8c140f65af9..0e55c3e715c 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -94,6 +94,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api.get_all_devices() + if entry.unique_id is None: + if api.smile_version[0] != "1.8.0": + hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname) + undo_listener = entry.add_update_listener(_update_listener) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 20c8a5a216c..689bfb68f22 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -96,6 +96,10 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.discovery_info: user_input[CONF_HOST] = self.discovery_info[CONF_HOST] + for entry in self._async_current_entries(): + if entry.data.get(CONF_HOST) == user_input[CONF_HOST]: + return self.async_abort(reason="already_configured") + try: api = await validate_input(self.hass, user_input) @@ -107,7 +111,9 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" if not errors: - await self.async_set_unique_id(api.gateway_id) + await self.async_set_unique_id( + api.smile_hostname or api.gateway_id, raise_on_progress=False + ) self._abort_if_unique_id_configured() return self.async_create_entry(title=api.smile_name, data=user_input) diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 8564b2c0d8c..11e077c8a24 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -73,6 +73,7 @@ def mock_smile_adam(): smile_mock.return_value.heater_id = "90986d591dcd426cae3ec3e8111ff730" smile_mock.return_value.smile_version = "3.0.15" smile_mock.return_value.smile_type = "thermostat" + smile_mock.return_value.smile_hostname = "smile98765" smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) smile_mock.return_value.full_update_device.side_effect = AsyncMock( @@ -112,6 +113,7 @@ def mock_smile_anna(): smile_mock.return_value.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927" smile_mock.return_value.smile_version = "4.0.15" smile_mock.return_value.smile_type = "thermostat" + smile_mock.return_value.smile_hostname = "smile98765" smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) smile_mock.return_value.full_update_device.side_effect = AsyncMock( @@ -151,6 +153,7 @@ def mock_smile_p1(): smile_mock.return_value.heater_id = None smile_mock.return_value.smile_version = "3.3.9" smile_mock.return_value.smile_type = "power" + smile_mock.return_value.smile_hostname = "smile98765" smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) smile_mock.return_value.full_update_device.side_effect = AsyncMock( diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 219ba8aee7f..e0f4993df55 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -111,6 +111,34 @@ async def test_zeroconf_form(hass): assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=TEST_DISCOVERY, + ) + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["errors"] == {} + + with patch( + "homeassistant.components.plugwise.config_flow.Smile.connect", + return_value=True, + ), patch( + "homeassistant.components.plugwise.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.plugwise.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {"password": TEST_PASSWORD}, + ) + + await hass.async_block_till_done() + + assert result4["type"] == "abort" + assert result4["reason"] == "already_configured" + async def test_form_invalid_auth(hass, mock_smile): """Test we handle invalid auth.""" From 4c2788a13c204b1401db8937e719a1c9373bcd1d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 13 Sep 2020 16:31:39 +0200 Subject: [PATCH 819/862] Improve handling of mireds being far out of spec (#40018) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_light.py | 26 ++++++++++++++++--- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 2cba87f74d6..6a47864375e 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==72"], + "requirements": ["pydeconz==73"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/requirements_all.txt b/requirements_all.txt index 0faab26721f..aa17999d7f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1295,7 +1295,7 @@ pydaikin==2.3.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==72 +pydeconz==73 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5519f7f3858..37fe829b0f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -625,7 +625,7 @@ pycountry==19.8.18 pydaikin==2.3.1 # homeassistant.components.deconz -pydeconz==72 +pydeconz==73 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index d070bd5b420..4e9f6b3d512 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -67,6 +67,15 @@ LIGHTS = { "type": "On and Off light", "uniqueid": "00:00:00:00:00:00:00:03-00", }, + "5": { + "ctmax": 1000, + "ctmin": 0, + "id": "Tunable white light with bad maxmin values id", + "name": "Tunable white light with bad maxmin values", + "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, + "type": "Tunable white light", + "uniqueid": "00:00:00:00:00:00:00:04-00", + }, } @@ -101,7 +110,7 @@ async def test_lights_and_groups(hass): assert "light.on_off_switch" not in gateway.deconz_ids assert "light.on_off_light" in gateway.deconz_ids - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == "on" @@ -117,6 +126,15 @@ async def test_lights_and_groups(hass): assert tunable_white_light.attributes["min_mireds"] == 155 assert tunable_white_light.attributes["supported_features"] == 2 + tunable_white_light_bad_maxmin = hass.states.get( + "light.tunable_white_light_with_bad_maxmin_values" + ) + assert tunable_white_light_bad_maxmin.state == "on" + assert tunable_white_light_bad_maxmin.attributes["color_temp"] == 2500 + assert tunable_white_light_bad_maxmin.attributes["max_mireds"] == 650 + assert tunable_white_light_bad_maxmin.attributes["min_mireds"] == 140 + assert tunable_white_light_bad_maxmin.attributes["supported_features"] == 2 + on_off_light = hass.states.get("light.on_off_light") assert on_off_light.state == "on" assert on_off_light.attributes["supported_features"] == 0 @@ -256,7 +274,7 @@ async def test_disable_light_groups(hass): assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 rgb_light = hass.states.get("light.rgb_light") assert rgb_light is not None @@ -281,7 +299,7 @@ async def test_disable_light_groups(hass): assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 hass.config_entries.async_update_entry( gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False} @@ -294,4 +312,4 @@ async def test_disable_light_groups(hass): assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 From 4ee7cdc8a0baebce17bf319129023a5588a2088b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 09:44:37 -0500 Subject: [PATCH 820/862] Do not log an error when a host is unreachable while pinging (#40024) --- homeassistant/components/ping/binary_sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index ac73da0a13f..79e0268c77b 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -210,7 +210,8 @@ class PingDataSubProcess(PingData): out_error, ) - if pinger.returncode != 0: + if pinger.returncode > 1: + # returncode of 1 means the host is unreachable _LOGGER.exception( "Error running command: `%s`, return code: %s", " ".join(self._ping_cmd), From a971b92899d01c47d9b7cd10d2c1c02d9a00a8de Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Sep 2020 18:23:50 +0200 Subject: [PATCH 821/862] Fix slack notifications requiring an icon (#40027) --- homeassistant/components/slack/notify.py | 29 ++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index d43a1fc25ad..8477b2fb501 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -202,22 +202,23 @@ class SlackNotificationService(BaseNotificationService): self, targets, message, title, blocks, username, icon ): """Send a text-only message.""" - if self._icon.lower().startswith(("http://", "https://")): - icon_type = "url" - else: - icon_type = "emoji" + message_dict = { + "blocks": blocks, + "link_names": True, + "text": message, + "username": username, + } + + if self._icon: + if self._icon.lower().startswith(("http://", "https://")): + icon_type = "url" + else: + icon_type = "emoji" + + message_dict[f"icon_{icon_type}"] = icon tasks = { - target: self._client.chat_postMessage( - **{ - "blocks": blocks, - "channel": target, - "link_names": True, - "text": message, - "username": username, - f"icon_{icon_type}": icon, - } - ) + target: self._client.chat_postMessage(**message_dict, channel=target) for target in targets } From 30b856554879e0b23a915c39bc4c0c5e93ecda9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2020 14:55:49 -0500 Subject: [PATCH 822/862] Ensure homekit_controller traps exceptions from find_ip_by_device_id (#40030) --- homeassistant/components/homekit_controller/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 7e98fc40910..9ca247382c7 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -304,9 +304,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # Its possible that the first try may have been busy so # we always check to see if self.finish_paring has been # set. - discovery = await self.controller.find_ip_by_device_id(self.hkid) - try: + discovery = await self.controller.find_ip_by_device_id(self.hkid) self.finish_pairing = await discovery.start_pairing(self.hkid) except aiohomekit.BusyError: From afde5a7ece73bccd71307ee78b7e4c6c41206a51 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 13 Sep 2020 22:05:45 +0200 Subject: [PATCH 823/862] Fix entity extraction from Template conditions (#40034) --- homeassistant/helpers/condition.py | 10 ++++++++-- tests/helpers/test_condition.py | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 1b09348415c..f67b9a4b0ab 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -649,13 +649,16 @@ async def async_validate_condition_config( @callback -def async_extract_entities(config: ConfigType) -> Set[str]: +def async_extract_entities(config: Union[ConfigType, Template]) -> Set[str]: """Extract entities from a condition.""" referenced: Set[str] = set() to_process = deque([config]) while to_process: config = to_process.popleft() + if isinstance(config, Template): + continue + condition = config[CONF_CONDITION] if condition in ("and", "not", "or"): @@ -674,13 +677,16 @@ def async_extract_entities(config: ConfigType) -> Set[str]: @callback -def async_extract_devices(config: ConfigType) -> Set[str]: +def async_extract_devices(config: Union[ConfigType, Template]) -> Set[str]: """Extract devices from a condition.""" referenced = set() to_process = deque([config]) while to_process: config = to_process.popleft() + if isinstance(config, Template): + continue + condition = config[CONF_CONDITION] if condition in ("and", "not", "or"): diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index af01163bfd5..71770d21186 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -5,6 +5,7 @@ import pytest from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition +from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -807,6 +808,7 @@ async def test_extract_entities(): "entity_id": ["sensor.temperature_9", "sensor.temperature_10"], "below": 110, }, + Template("{{ is_state('light.example', 'on') }}"), ], } ) == { @@ -867,6 +869,7 @@ async def test_extract_devices(): }, ], }, + Template("{{ is_state('light.example', 'on') }}"), ], } ) From ef1649383ca37182bedfc2746b231505cda50f65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Sep 2020 20:22:47 +0000 Subject: [PATCH 824/862] Bumped version to 0.115.0b9 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a1df155f1e7..9680e20161b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b8" +PATCH_VERSION = "0b9" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From db27079fa8ee2e2db7faa5ca21067041745b88fc Mon Sep 17 00:00:00 2001 From: r4nd0mbr1ck <23737685+r4nd0mbr1ck@users.noreply.github.com> Date: Tue, 15 Sep 2020 15:50:44 +1000 Subject: [PATCH 825/862] Speedtestdotnet - use server name to generate server list (#39775) --- homeassistant/components/speedtestdotnet/__init__.py | 7 +++++-- tests/components/speedtestdotnet/__init__.py | 4 ++-- tests/components/speedtestdotnet/test_config_flow.py | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 57557d4558a..32562251ed4 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -143,9 +143,12 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): self.servers[DEFAULT_SERVER] = {} for server in sorted( - server_list.values(), key=lambda server: server[0]["country"] + server_list.values(), + key=lambda server: server[0]["country"] + server[0]["sponsor"], ): - self.servers[f"{server[0]['country']} - {server[0]['sponsor']}"] = server[0] + self.servers[ + f"{server[0]['country']} - {server[0]['sponsor']} - {server[0]['name']}" + ] = server[0] def update_data(self): """Get the latest data from speedtest.net.""" diff --git a/tests/components/speedtestdotnet/__init__.py b/tests/components/speedtestdotnet/__init__.py index f67a633e25f..f6f64b9c7bb 100644 --- a/tests/components/speedtestdotnet/__init__.py +++ b/tests/components/speedtestdotnet/__init__.py @@ -9,7 +9,7 @@ MOCK_SERVERS = { "name": "Server1", "country": "Country1", "cc": "LL1", - "sponsor": "Server1", + "sponsor": "Sponsor1", "id": "1", "host": "server1:8080", "d": 1, @@ -23,7 +23,7 @@ MOCK_SERVERS = { "name": "Server2", "country": "Country2", "cc": "LL2", - "sponsor": "server2", + "sponsor": "Sponsor2", "id": "2", "host": "server2:8080", "d": 2, diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index cfd79fb38f8..8e7edc2d986 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -108,7 +108,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_SERVER_NAME: "Country1 - Server1", + CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SCAN_INTERVAL: 30, CONF_MANUAL: False, }, @@ -116,7 +116,7 @@ async def test_options(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { - CONF_SERVER_NAME: "Country1 - Server1", + CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SERVER_ID: "1", CONF_SCAN_INTERVAL: 30, CONF_MANUAL: False, From 1a76a953c739818c89cb84d43f64bc3dd973e0f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Sep 2020 01:48:30 -0500 Subject: [PATCH 826/862] Update gogogate2-api to 2.0.2 (#40010) * Update gogogate2-api to 2.0.2 Resolves a timeout issue: https://github.com/vangorra/python_gogogate2_api/pull/11 * mock voltage --- homeassistant/components/gogogate2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gogogate2/test_cover.py | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index edf69144f62..a12058d38d0 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -3,7 +3,7 @@ "name": "Gogogate2 and iSmartGate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["gogogate2-api==2.0.1"], + "requirements": ["gogogate2-api==2.0.2"], "codeowners": ["@vangorra"], "homekit": { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index aa17999d7f1..930c388fea3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -669,7 +669,7 @@ glances_api==0.2.0 gntp==1.0.3 # homeassistant.components.gogogate2 -gogogate2-api==2.0.1 +gogogate2-api==2.0.2 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37fe829b0f1..d076089a6d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -334,7 +334,7 @@ gios==0.1.4 glances_api==0.2.0 # homeassistant.components.gogogate2 -gogogate2-api==2.0.1 +gogogate2-api==2.0.2 # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index eb2907e2b6e..b1ab2284580 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -110,6 +110,7 @@ async def test_import( camera=False, events=2, temperature=None, + voltage=40, ), door2=GogoGate2Door( door_id=2, @@ -123,6 +124,7 @@ async def test_import( camera=False, events=0, temperature=None, + voltage=40, ), door3=GogoGate2Door( door_id=3, @@ -136,6 +138,7 @@ async def test_import( camera=False, events=0, temperature=None, + voltage=40, ), outputs=Outputs(output1=True, output2=False, output3=True), network=Network(ip=""), @@ -170,6 +173,7 @@ async def test_import( enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), door2=ISmartGateDoor( door_id=1, @@ -186,6 +190,7 @@ async def test_import( enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), door3=ISmartGateDoor( door_id=1, @@ -202,6 +207,7 @@ async def test_import( enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), network=Network(ip=""), wifi=Wifi(SSID="", linkquality="", signal=""), @@ -268,6 +274,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: camera=False, events=2, temperature=None, + voltage=40, ), door2=GogoGate2Door( door_id=2, @@ -281,6 +288,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: camera=False, events=0, temperature=None, + voltage=40, ), door3=GogoGate2Door( door_id=3, @@ -294,6 +302,7 @@ async def test_open_close_update(gogogat2api_mock, hass: HomeAssistant) -> None: camera=False, events=0, temperature=None, + voltage=40, ), outputs=Outputs(output1=True, output2=False, output3=True), network=Network(ip=""), @@ -381,6 +390,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), door2=ISmartGateDoor( door_id=2, @@ -397,6 +407,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), door3=ISmartGateDoor( door_id=3, @@ -413,6 +424,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: enabled=True, apicode="apicode0", customimage=False, + voltage=40, ), network=Network(ip=""), wifi=Wifi(SSID="", linkquality="", signal=""), From 7a7cad39eb59314e3018535e7697234661559785 Mon Sep 17 00:00:00 2001 From: b3nj1 Date: Mon, 14 Sep 2020 12:29:51 -0700 Subject: [PATCH 827/862] Fix ecobee weather forcast off by 1 bug (#40048) --- homeassistant/components/ecobee/weather.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 4ea90d27106..71c1d263e02 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -1,5 +1,5 @@ """Support for displaying weather info from Ecobee API.""" -from datetime import datetime +from datetime import timedelta from pyecobee.const import ECOBEE_STATE_UNKNOWN @@ -13,6 +13,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.util import dt as dt_util from .const import ( _LOGGER, @@ -165,10 +166,13 @@ class EcobeeWeather(WeatherEntity): return None forecasts = [] - for day in range(1, 5): + date = dt_util.utcnow() + for day in range(0, 5): forecast = _process_forecast(self.weather["forecasts"][day]) if forecast is None: continue + forecast[ATTR_FORECAST_TIME] = date.isoformat() + date += timedelta(days=1) forecasts.append(forecast) if forecasts: @@ -186,9 +190,6 @@ def _process_forecast(json): """Process a single ecobee API forecast to return expected values.""" forecast = {} try: - forecast[ATTR_FORECAST_TIME] = datetime.strptime( - json["dateTime"], "%Y-%m-%d %H:%M:%S" - ).isoformat() forecast[ATTR_FORECAST_CONDITION] = ECOBEE_WEATHER_SYMBOL_TO_HASS[ json["weatherSymbol"] ] From a9d24c2cd5c0d16c4089ed9a71352b15c2ad3906 Mon Sep 17 00:00:00 2001 From: Markus Bong <2Fake1987@gmail.com> Date: Mon, 14 Sep 2020 12:24:49 +0200 Subject: [PATCH 828/862] Correct devolo climate devices (#40061) --- homeassistant/components/devolo_home_control/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index d44a0c981f1..05f4363c384 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -27,7 +27,7 @@ async def async_setup_entry( for device in hass.data[DOMAIN]["homecontrol"].multi_level_switch_devices: for multi_level_switch in device.multi_level_switch_property: - if device.deviceModelUID in [ + if device.device_model_uid in [ "devolo.model.Thermostat:Valve", "devolo.model.Room:Thermostat", ]: From 0e823b566bcdee4b2987de656da3f1c9c084f8bb Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 14 Sep 2020 14:36:08 +0200 Subject: [PATCH 829/862] Fix default forecast mode OpenWeatherMap (#40062) --- homeassistant/components/openweathermap/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 365f55c5d44..5f960e52ecf 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -91,7 +91,7 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if CONF_LONGITUDE not in config: config[CONF_LONGITUDE] = self.hass.config.longitude if CONF_MODE not in config: - config[CONF_MODE] = DEFAULT_LANGUAGE + config[CONF_MODE] = DEFAULT_FORECAST_MODE if CONF_LANGUAGE not in config: config[CONF_LANGUAGE] = DEFAULT_LANGUAGE return await self.async_step_user(config) From e0fcf9b6484ae52dcd0d83c432c70b6c260380d1 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 14 Sep 2020 13:18:43 +0100 Subject: [PATCH 830/862] Bump aiohomekit version (regression fix) (#40064) --- .../components/homekit_controller/manifest.json | 16 ++++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 1ee2f16ffcf..1fb4c05c595 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,8 +3,16 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit[IP]==0.2.49"], - "zeroconf": ["_hap._tcp.local."], - "after_dependencies": ["zeroconf"], - "codeowners": ["@Jc2k"] + "requirements": [ + "aiohomekit==0.2.53" + ], + "zeroconf": [ + "_hap._tcp.local." + ], + "after_dependencies": [ + "zeroconf" + ], + "codeowners": [ + "@Jc2k" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index 930c388fea3..2ab44ff224b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.49 +aiohomekit==0.2.53 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d076089a6d8..1f6237fd130 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ aioguardian==1.0.1 aioharmony==0.2.6 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.49 +aiohomekit==0.2.53 # homeassistant.components.emulated_hue # homeassistant.components.http From a38e047e83765363f56de168f66d63cc19298e58 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 14 Sep 2020 14:50:39 +0200 Subject: [PATCH 831/862] Update docker base image to 8.4.0 (#40066) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index 6d8763a019f..d63a245793b 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:8.3.0", - "armhf": "homeassistant/armhf-homeassistant-base:8.3.0", - "armv7": "homeassistant/armv7-homeassistant-base:8.3.0", - "amd64": "homeassistant/amd64-homeassistant-base:8.3.0", - "i386": "homeassistant/i386-homeassistant-base:8.3.0" + "aarch64": "homeassistant/aarch64-homeassistant-base:8.4.0", + "armhf": "homeassistant/armhf-homeassistant-base:8.4.0", + "armv7": "homeassistant/armv7-homeassistant-base:8.4.0", + "amd64": "homeassistant/amd64-homeassistant-base:8.4.0", + "i386": "homeassistant/i386-homeassistant-base:8.4.0" }, "labels": { "io.hass.type": "core" From 3ef821d62f664f12d981f398bf64a5c3e387d17f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Sep 2020 15:40:32 +0200 Subject: [PATCH 832/862] Fix tag last scanned serialization (#40067) --- homeassistant/components/tag/__init__.py | 4 ++-- tests/components/tag/test_init.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 872d097d5de..0b445fbf575 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -70,7 +70,7 @@ class TagStorageCollection(collection.StorageCollection): data[TAG_ID] = str(uuid.uuid4()) # make last_scanned JSON serializeable if LAST_SCANNED in data: - data[LAST_SCANNED] = str(data[LAST_SCANNED]) + data[LAST_SCANNED] = data[LAST_SCANNED].isoformat() return data @callback @@ -83,7 +83,7 @@ class TagStorageCollection(collection.StorageCollection): data = {**data, **self.UPDATE_SCHEMA(update_data)} # make last_scanned JSON serializeable if LAST_SCANNED in data: - data[LAST_SCANNED] = str(data[LAST_SCANNED]) + data[LAST_SCANNED] = data[LAST_SCANNED].isoformat() return data diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index d10a59ef2f0..e4b810e0661 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -6,6 +6,9 @@ import pytest from homeassistant.components.tag import DOMAIN, TAGS, async_scan_tag from homeassistant.helpers import collection from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.async_mock import patch _LOGGER = logging.getLogger(__name__) @@ -60,7 +63,10 @@ async def test_tag_scanned(hass, hass_ws_client, storage_setup): assert len(result) == 1 assert "test tag" in result - await async_scan_tag(hass, "new tag", "some_scanner") + now = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=now): + await async_scan_tag(hass, "new tag", "some_scanner") + await client.send_json({"id": 7, "type": f"{DOMAIN}/list"}) resp = await client.receive_json() assert resp["success"] @@ -70,7 +76,7 @@ async def test_tag_scanned(hass, hass_ws_client, storage_setup): assert len(result) == 2 assert "test tag" in result assert "new tag" in result - assert result["new tag"]["last_scanned"] is not None + assert result["new tag"]["last_scanned"] == now.isoformat() def track_changes(coll: collection.ObservableCollection): From 99a86046015e8021d225dc796c326ad668072106 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 15 Sep 2020 00:32:20 +0200 Subject: [PATCH 833/862] Fix netatmo media browser of outdoor events (#40079) * Fix outdoor events * Fix test data * Increase coverage --- homeassistant/components/netatmo/camera.py | 6 +-- .../components/netatmo/media_source.py | 16 ++++++- tests/components/netatmo/test_media_source.py | 42 ++++++++++++++++++- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 3f9720f3adb..dff6013c7c6 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -284,9 +284,9 @@ class NetatmoCamera(NetatmoBase, Camera): self._data.events.get(self._id, {}) ) elif self._model == "NOC": # Smart Outdoor Camera - self.hass.data[DOMAIN][DATA_EVENTS][ - self._id - ] = self._data.outdoor_events.get(self._id, {}) + self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( + self._data.outdoor_events.get(self._id, {}) + ) def process_events(self, events): """Add meta data to events.""" diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index 76527677224..6375c46d394 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -80,8 +80,20 @@ class NetatmoSource(MediaSource): ) -> BrowseMediaSource: if event_id and event_id in self.events[camera_id]: created = dt.datetime.fromtimestamp(event_id) - thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url") - message = remove_html_tags(self.events[camera_id][event_id]["message"]) + if self.events[camera_id][event_id]["type"] == "outdoor": + thumbnail = ( + self.events[camera_id][event_id]["event_list"][0] + .get("snapshot", {}) + .get("url") + ) + message = remove_html_tags( + self.events[camera_id][event_id]["event_list"][0]["message"] + ) + else: + thumbnail = ( + self.events[camera_id][event_id].get("snapshot", {}).get("url") + ) + message = remove_html_tags(self.events[camera_id][event_id]["message"]) title = f"{created} - {message}" else: title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER) diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index 0405317f03e..1773c0d83da 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -18,6 +18,7 @@ async def test_async_browse_media(hass): "12:34:56:78:90:ab": { 1599152672: { "id": "12345", + "type": "person", "time": 1599152672, "camera_id": "12:34:56:78:90:ab", "snapshot": { @@ -30,6 +31,7 @@ async def test_async_browse_media(hass): }, 1599152673: { "id": "12346", + "type": "person", "time": 1599152673, "camera_id": "12:34:56:78:90:ab", "snapshot": { @@ -37,9 +39,47 @@ async def test_async_browse_media(hass): }, "message": "Tobias seen", }, + 1599152674: { + "id": "12347", + "type": "outdoor", + "time": 1599152674, + "camera_id": "12:34:56:78:90:ac", + "snapshot": { + "url": "https://netatmocameraimage", + }, + "video_id": "98766", + "video_status": "available", + "event_list": [ + { + "type": "vehicle", + "time": 1599152674, + "id": "12347-0", + "offset": 0, + "message": "Vehicle detected", + "snapshot": { + "url": "https://netatmocameraimage", + }, + }, + { + "type": "human", + "time": 1599152674, + "id": "12347-1", + "offset": 8, + "message": "Person detected", + "snapshot": { + "url": "https://netatmocameraimage", + }, + }, + ], + "media_url": "http:///files/high/index.m3u8", + }, } } - hass.data[DOMAIN][DATA_CAMERAS] = {"12:34:56:78:90:ab": "MyCamera"} + + hass.data[DOMAIN][DATA_CAMERAS] = { + "12:34:56:78:90:ab": "MyCamera", + "12:34:56:78:90:ac": "MyOutdoorCamera", + } assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() From e5c499c22e16b391730ca1b06e027d43c311ee09 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Sep 2020 22:10:30 +0200 Subject: [PATCH 834/862] Increase TIMEOUT_ACK to 2s (#40080) --- homeassistant/components/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 2b5dca6474f..470e43b5b44 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -134,7 +134,7 @@ CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" DISCOVERY_COOLDOWN = 2 -TIMEOUT_ACK = 1 +TIMEOUT_ACK = 2 PLATFORMS = [ "alarm_control_panel", From 24fe9cdd5ac2d2ece7e87456faae42967d34ba14 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 14 Sep 2020 16:48:39 -0400 Subject: [PATCH 835/862] Update ZHA dependency (#40083) --- 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 4bde073a933..c6b0fa78799 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.20.1", + "bellows==0.20.2", "pyserial==3.4", "zha-quirks==0.0.44", "zigpy-cc==0.5.2", diff --git a/requirements_all.txt b/requirements_all.txt index 2ab44ff224b..5bde6df6972 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ beautifulsoup4==4.9.1 # beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows==0.20.1 +bellows==0.20.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f6237fd130..7a9fb0efb70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -183,7 +183,7 @@ azure-eventhub==5.1.0 base36==0.1.1 # homeassistant.components.zha -bellows==0.20.1 +bellows==0.20.2 # homeassistant.components.blebox blebox_uniapi==1.3.2 From 81436fb68811256edbaad6c23f7652b1bf1fdcd9 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 15 Sep 2020 09:30:00 +0200 Subject: [PATCH 836/862] Check Sonos for local library before browsing (#40085) --- homeassistant/components/sonos/media_player.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2b50f2864dc..307fee923a3 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1547,6 +1547,13 @@ def library_payload(media_library): Used by async_browse_media. """ + if not media_library.browse_by_idstring( + "tracks", + "", + max_items=1, + ): + raise BrowseError("Local library not found") + children = [] for item in media_library.browse(): try: From 610a327b529215e9fe492308676e5cae4a098a79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Sep 2020 02:27:30 -0500 Subject: [PATCH 837/862] Convert color temperature to hue and saturation for HomeKit (#40089) The HomeKit spec does not permit the color temp characteristic being exposed when color (hue, sat) is present. Since Home Assistant will still send color temp values, we need to convert them to hue, sat values for HomeKit --- homeassistant/components/homekit/type_lights.py | 17 ++++++++++++++--- tests/components/homekit/test_type_lights.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 612d8e53a02..086934ea6f7 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -24,6 +24,10 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import callback +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin, + color_temperature_to_hs, +) from .accessories import TYPES, HomeAccessory from .const import ( @@ -64,8 +68,6 @@ class Light(HomeAccessory): if self._features & SUPPORT_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) - self._hue = None - self._saturation = None elif self._features & SUPPORT_COLOR_TEMP: # ColorTemperature and Hue characteristic should not be # exposed both. Both states are tracked separately in HomeKit, @@ -179,7 +181,16 @@ class Light(HomeAccessory): # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: - hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None)) + if ATTR_HS_COLOR in new_state.attributes: + hue, saturation = new_state.attributes[ATTR_HS_COLOR] + elif ATTR_COLOR_TEMP in new_state.attributes: + hue, saturation = color_temperature_to_hs( + color_temperature_mired_to_kelvin( + new_state.attributes[ATTR_COLOR_TEMP] + ) + ) + else: + hue, saturation = None, None if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): hue = round(hue, 0) saturation = round(saturation, 0) diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 20029861adb..e82bc5bb15d 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -293,9 +293,25 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event ) await hass.async_block_till_done() acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + assert acc.char_hue.value == 260 + assert acc.char_saturation.value == 90 assert not hasattr(acc, "char_color_temperature") + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 224}) + await hass.async_block_till_done() + await acc.run_handler() + await hass.async_block_till_done() + assert acc.char_hue.value == 27 + assert acc.char_saturation.value == 27 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: 352}) + await hass.async_block_till_done() + await acc.run_handler() + await hass.async_block_till_done() + assert acc.char_hue.value == 28 + assert acc.char_saturation.value == 61 + async def test_light_rgb_color(hass, hk_driver, cls, events): """Test light with rgb_color.""" From 3ef3d848f73d2b7e1dd484e4bdb10c512f41af3a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 15 Sep 2020 15:57:10 +0200 Subject: [PATCH 838/862] Update frontend to 20200915.0 (#40101) --- 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 d5b48a9b185..9b499f0376a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200912.0"], + "requirements": ["home-assistant-frontend==20200915.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 98c6a02efa2..ccede9062a0 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.37.0 -home-assistant-frontend==20200912.0 +home-assistant-frontend==20200915.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5bde6df6972..97e840bea16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200912.0 +home-assistant-frontend==20200915.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a9fb0efb70..e32ce166be0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200912.0 +home-assistant-frontend==20200915.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From e095120023de7d32120c5ba9b5ec80c3ad89358a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 15 Sep 2020 16:21:32 +0200 Subject: [PATCH 839/862] Bumped version to 0.115.0b10 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9680e20161b..291bb110a99 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b9" +PATCH_VERSION = "0b10" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From ce86112612db26f179bdf9aad175169a5a93a5a6 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 15 Sep 2020 19:01:36 +0100 Subject: [PATCH 840/862] Address error in SQL query (#39939) --- homeassistant/components/sql/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index f19941ed043..27656c260d3 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -122,6 +122,7 @@ class SQLSensor(Entity): def update(self): """Retrieve sensor data from the query.""" + data = None try: sess = self.sessionmaker() result = sess.execute(self._query) @@ -147,7 +148,7 @@ class SQLSensor(Entity): finally: sess.close() - if self._template is not None: + if data is not None and self._template is not None: self._state = self._template.async_render_with_possible_json_value( data, None ) From 5d518b5365cbacb70367f113f42c43ffa2cb153a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 15:28:25 +0200 Subject: [PATCH 841/862] Add media dirs core configuration (#40071) Co-authored-by: Paulus Schoutsen --- .../components/media_source/const.py | 4 +- .../components/media_source/local_source.py | 98 +++++++++++++------ homeassistant/config.py | 13 ++- homeassistant/const.py | 1 + homeassistant/core.py | 3 + tests/common.py | 1 + tests/components/media_source/test_init.py | 19 +++- .../media_source/test_local_source.py | 39 +++++++- tests/test_config.py | 19 ++++ 9 files changed, 157 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py index 68a8244c3ce..739af47e653 100644 --- a/homeassistant/components/media_source/const.py +++ b/homeassistant/components/media_source/const.py @@ -15,4 +15,6 @@ MEDIA_CLASS_MAP = { "image": MEDIA_CLASS_IMAGE, } URI_SCHEME = "media-source://" -URI_SCHEME_REGEX = re.compile(r"^media-source://(?P[^/]+)?(?P.+)?") +URI_SCHEME_REGEX = re.compile( + r"^media-source:\/\/(?:(?P(?!.+__)(?!_)[\da-z_]+(?(?!\/).+))?)?$" +) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index a558de775f8..e14735fb60b 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -21,26 +21,7 @@ def async_setup(hass: HomeAssistant): """Set up local media source.""" source = LocalSource(hass) hass.data[DOMAIN][DOMAIN] = source - hass.http.register_view(LocalMediaView(hass)) - - -@callback -def async_parse_identifier(item: MediaSourceItem) -> Tuple[str, str]: - """Parse identifier.""" - if not item.identifier: - source_dir_id = "media" - location = "" - - else: - source_dir_id, location = item.identifier.lstrip("/").split("/", 1) - - if source_dir_id != "media": - raise Unresolvable("Unknown source directory.") - - if location != sanitize_path(location): - raise Unresolvable("Invalid path.") - - return source_dir_id, location + hass.http.register_view(LocalMediaView(hass, source)) class LocalSource(MediaSource): @@ -56,22 +37,41 @@ class LocalSource(MediaSource): @callback def async_full_path(self, source_dir_id, location) -> Path: """Return full path.""" - return self.hass.config.path("media", location) + return Path(self.hass.config.media_dirs[source_dir_id], location) + + @callback + def async_parse_identifier(self, item: MediaSourceItem) -> Tuple[str, str]: + """Parse identifier.""" + if not item.identifier: + # Empty source_dir_id and location + return "", "" + + source_dir_id, location = item.identifier.split("/", 1) + if source_dir_id not in self.hass.config.media_dirs: + raise Unresolvable("Unknown source directory.") + + if location != sanitize_path(location): + raise Unresolvable("Invalid path.") + + return source_dir_id, location async def async_resolve_media(self, item: MediaSourceItem) -> str: """Resolve media to a url.""" - source_dir_id, location = async_parse_identifier(item) + source_dir_id, location = self.async_parse_identifier(item) + if source_dir_id == "" or source_dir_id not in self.hass.config.media_dirs: + raise Unresolvable("Unknown source directory.") + mime_type, _ = mimetypes.guess_type( - self.async_full_path(source_dir_id, location) + str(self.async_full_path(source_dir_id, location)) ) - return PlayMedia(item.identifier, mime_type) + return PlayMedia(f"/local_source/{item.identifier}", mime_type) async def async_browse_media( self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES ) -> BrowseMediaSource: """Return media.""" try: - source_dir_id, location = async_parse_identifier(item) + source_dir_id, location = self.async_parse_identifier(item) except Unresolvable as err: raise BrowseError(str(err)) from err @@ -79,9 +79,37 @@ class LocalSource(MediaSource): self._browse_media, source_dir_id, location ) - def _browse_media(self, source_dir_id, location): + def _browse_media(self, source_dir_id: str, location: Path): """Browse media.""" - full_path = Path(self.hass.config.path("media", location)) + + # If only one media dir is configured, use that as the local media root + if source_dir_id == "" and len(self.hass.config.media_dirs) == 1: + source_dir_id = list(self.hass.config.media_dirs)[0] + + # Multiple folder, root is requested + if source_dir_id == "": + if location: + raise BrowseError("Folder not found.") + + base = BrowseMediaSource( + domain=DOMAIN, + identifier="", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=None, + title=self.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + base.children = [ + self._browse_media(source_dir_id, "") + for source_dir_id in self.hass.config.media_dirs + ] + + return base + + full_path = Path(self.hass.config.media_dirs[source_dir_id], location) if not full_path.exists(): if location == "": @@ -118,7 +146,7 @@ class LocalSource(MediaSource): media = BrowseMediaSource( domain=DOMAIN, - identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}", + identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.media_dirs[source_dir_id])}", media_class=media_class, media_content_type=mime_type or "", title=title, @@ -149,19 +177,25 @@ class LocalMediaView(HomeAssistantView): Returns media files in config/media. """ - url = "/media/{location:.*}" + url = "/local_source/{source_dir_id}/{location:.*}" name = "media" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant, source: LocalSource): """Initialize the media view.""" self.hass = hass + self.source = source - async def get(self, request: web.Request, location: str) -> web.FileResponse: + async def get( + self, request: web.Request, source_dir_id: str, location: str + ) -> web.FileResponse: """Start a GET request.""" if location != sanitize_path(location): return web.HTTPNotFound() - media_path = Path(self.hass.config.path("media", location)) + if source_dir_id not in self.hass.config.media_dirs: + return web.HTTPNotFound() + + media_path = self.source.async_full_path(source_dir_id, location) # Check that the file exists if not media_path.is_file(): diff --git a/homeassistant/config.py b/homeassistant/config.py index 36a81f98fa3..3d6e3fb041c 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -33,6 +33,7 @@ from homeassistant.const import ( CONF_INTERNAL_URL, CONF_LATITUDE, CONF_LONGITUDE, + CONF_MEDIA_DIRS, CONF_NAME, CONF_PACKAGES, CONF_TEMPERATURE_UNIT, @@ -221,6 +222,8 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( ], _no_duplicate_auth_mfa_module, ), + # pylint: disable=no-value-for-parameter + vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), } ) @@ -485,6 +488,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non CONF_UNIT_SYSTEM, CONF_EXTERNAL_URL, CONF_INTERNAL_URL, + CONF_MEDIA_DIRS, ] ): hac.config_source = SOURCE_YAML @@ -496,6 +500,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non (CONF_ELEVATION, "elevation"), (CONF_INTERNAL_URL, "internal_url"), (CONF_EXTERNAL_URL, "external_url"), + (CONF_MEDIA_DIRS, "media_dirs"), ): if key in config: setattr(hac, attr, config[key]) @@ -503,8 +508,14 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non if CONF_TIME_ZONE in config: hac.set_time_zone(config[CONF_TIME_ZONE]) + if CONF_MEDIA_DIRS not in config: + if is_docker_env(): + hac.media_dirs = {"media": "/media"} + else: + hac.media_dirs = {"media": hass.config.path("media")} + # Init whitelist external dir - hac.allowlist_external_dirs = {hass.config.path("www"), hass.config.path("media")} + hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()} if CONF_ALLOWLIST_EXTERNAL_DIRS in config: hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS])) diff --git a/homeassistant/const.py b/homeassistant/const.py index 291bb110a99..ec1f366fabe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -116,6 +116,7 @@ CONF_LIGHTS = "lights" CONF_LONGITUDE = "longitude" CONF_MAC = "mac" CONF_MAXIMUM = "maximum" +CONF_MEDIA_DIRS = "media_dirs" CONF_METHOD = "method" CONF_MINIMUM = "minimum" CONF_MODE = "mode" diff --git a/homeassistant/core.py b/homeassistant/core.py index 8f3809bbd4c..f230fef01eb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1390,6 +1390,9 @@ class Config: # List of allowed external URLs that integrations may use self.allowlist_external_urls: Set[str] = set() + # Dictionary of Media folders that integrations may use + self.media_dirs: Dict[str, str] = {} + # If Home Assistant is running in safe mode self.safe_mode: bool = False diff --git a/tests/common.py b/tests/common.py index 1cba478767f..b36439d4110 100644 --- a/tests/common.py +++ b/tests/common.py @@ -205,6 +205,7 @@ async def async_test_home_assistant(loop): hass.config.elevation = 0 hass.config.time_zone = date_util.get_time_zone("US/Pacific") hass.config.units = METRIC_SYSTEM + hass.config.media_dirs = {"media": get_test_config_dir("media")} hass.config.skip_pip = True hass.config_entries = config_entries.ConfigEntries(hass, {}) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 68e0fcda1d8..c7fc2dd6338 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -5,6 +5,7 @@ from homeassistant.components import media_source from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source import const +from homeassistant.components.media_source.error import Unresolvable from homeassistant.setup import async_setup_component from tests.async_mock import patch @@ -62,11 +63,23 @@ async def test_async_resolve_media(hass): assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() - # Test no media content - media = await media_source.async_resolve_media(hass, "") + media = await media_source.async_resolve_media( + hass, + media_source.generate_media_source_id(const.DOMAIN, "media/test.mp3"), + ) assert isinstance(media, media_source.models.PlayMedia) +async def test_async_unresolve_media(hass): + """Test browse media.""" + assert await async_setup_component(hass, const.DOMAIN, {}) + await hass.async_block_till_done() + + # Test no media content + with pytest.raises(Unresolvable): + await media_source.async_resolve_media(hass, "") + + async def test_websocket_browse_media(hass, hass_ws_client): """Test browse media websocket.""" assert await async_setup_component(hass, const.DOMAIN, {}) @@ -127,7 +140,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client): client = await hass_ws_client(hass) - media = media_source.models.PlayMedia("/media/test.mp3", "audio/mpeg") + media = media_source.models.PlayMedia("/local_source/media/test.mp3", "audio/mpeg") with patch( "homeassistant.components.media_source.async_resolve_media", diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index 44d38107949..bd0a1435eef 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -3,11 +3,18 @@ import pytest from homeassistant.components import media_source from homeassistant.components.media_source import const +from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component async def test_async_browse_media(hass): """Test browse media.""" + local_media = hass.config.path("media") + await async_process_ha_core_config( + hass, {"media_dirs": {"media": local_media, "recordings": local_media}} + ) + await hass.async_block_till_done() + assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() @@ -40,27 +47,53 @@ async def test_async_browse_media(hass): assert str(excinfo.value) == "Invalid path." # Test successful listing + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{const.DOMAIN}" + ) + assert media + media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/." ) assert media + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{const.DOMAIN}/recordings/." + ) + assert media + async def test_media_view(hass, hass_client): """Test media view.""" + local_media = hass.config.path("media") + await async_process_ha_core_config( + hass, {"media_dirs": {"media": local_media, "recordings": local_media}} + ) + await hass.async_block_till_done() + assert await async_setup_component(hass, const.DOMAIN, {}) await hass.async_block_till_done() client = await hass_client() # Protects against non-existent files - resp = await client.get("/media/invalid.txt") + resp = await client.get("/local_source/media/invalid.txt") + assert resp.status == 404 + + resp = await client.get("/local_source/recordings/invalid.txt") assert resp.status == 404 # Protects against non-media files - resp = await client.get("/media/not_media.txt") + resp = await client.get("/local_source/media/not_media.txt") + assert resp.status == 404 + + # Protects against unknown local media sources + resp = await client.get("/local_source/unknown_source/not_media.txt") assert resp.status == 404 # Fetch available media - resp = await client.get("/media/test.mp3") + resp = await client.get("/local_source/media/test.mp3") + assert resp.status == 200 + + resp = await client.get("/local_source/recordings/test.mp3") assert resp.status == 200 diff --git a/tests/test_config.py b/tests/test_config.py index c5443666bf5..a6c6ee86acc 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -440,6 +440,7 @@ async def test_loading_configuration(hass): "allowlist_external_dirs": "/etc", "external_url": "https://www.example.com", "internal_url": "http://example.local", + "media_dirs": {"mymedia": "/usr"}, }, ) @@ -453,6 +454,8 @@ async def test_loading_configuration(hass): assert hass.config.internal_url == "http://example.local" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs + assert "/usr" in hass.config.allowlist_external_dirs + assert hass.config.media_dirs == {"mymedia": "/usr"} assert hass.config.config_source == config_util.SOURCE_YAML @@ -483,6 +486,22 @@ async def test_loading_configuration_temperature_unit(hass): assert hass.config.config_source == config_util.SOURCE_YAML +async def test_loading_configuration_default_media_dirs_docker(hass): + """Test loading core config onto hass object.""" + with patch("homeassistant.config.is_docker_env", return_value=True): + await config_util.async_process_ha_core_config( + hass, + { + "name": "Huis", + }, + ) + + assert hass.config.location_name == "Huis" + assert len(hass.config.allowlist_external_dirs) == 2 + assert "/media" in hass.config.allowlist_external_dirs + assert hass.config.media_dirs == {"media": "/media"} + + async def test_loading_configuration_from_packages(hass): """Test loading packages config onto hass object config.""" await config_util.async_process_ha_core_config( From b856b0e15ddac30c8614d5e0650d2ebd798f7cd6 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 15 Sep 2020 17:29:24 +0300 Subject: [PATCH 842/862] Guard both Shelly 2 & Shelly 2.5 in roller mode (#40086) Co-authored-by: Maciej Bieniek --- homeassistant/components/shelly/switch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 0aaa6dbc911..0dcefb51cf9 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -14,7 +14,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): wrapper = hass.data[DOMAIN][config_entry.entry_id] # In roller mode the relay blocks exist but do not contain required info - if wrapper.model == "SHSW-25" and wrapper.device.settings["mode"] != "relay": + if ( + wrapper.model in ["SHSW-21", "SHSW-25"] + and wrapper.device.settings["mode"] != "relay" + ): return relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"] From 4518335a563e75a9521ff4dcabddd780ada0bed2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 15 Sep 2020 16:30:33 +0200 Subject: [PATCH 843/862] Remove the unnecessary prefix from the sensor names in the Shelly integration (#40097) --- homeassistant/components/shelly/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index b1a61bbbf59..96281e2aee1 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -42,11 +42,11 @@ async def async_setup_entry_attribute_entities( if not blocks: return - counts = Counter([item[0].type for item in blocks]) + counts = Counter([item[1] for item in blocks]) async_add_entities( [ - sensor_class(wrapper, block, sensor_id, description, counts[block.type]) + sensor_class(wrapper, block, sensor_id, description, counts[sensor_id]) for block, sensor_id, description in blocks ] ) From 39c4b338f138d34db6612fb22b54ad12e41bed87 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 16 Sep 2020 08:14:11 +0200 Subject: [PATCH 844/862] Increase TIMEOUT_ACK to 10s (#40117) --- homeassistant/components/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 470e43b5b44..d9bf1bbadfa 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -134,7 +134,7 @@ CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" DISCOVERY_COOLDOWN = 2 -TIMEOUT_ACK = 2 +TIMEOUT_ACK = 10 PLATFORMS = [ "alarm_control_panel", From 7f8a89838baa97d598108138a5bc362b92b6fb73 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 16 Sep 2020 00:12:59 +0200 Subject: [PATCH 845/862] Bump aioshelly library to version 0.3.2 (#40118) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b2d3a7b7795..16467fa999c 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.3.1"], + "requirements": ["aioshelly==0.3.2"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu"] } diff --git a/requirements_all.txt b/requirements_all.txt index 97e840bea16..5a9f88c4963 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.3.1 +aioshelly==0.3.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e32ce166be0..95fb575eee8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aiopvpc==2.0.2 aiopylgtv==0.3.3 # homeassistant.components.shelly -aioshelly==0.3.1 +aioshelly==0.3.2 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From d3bb2e5e16ba823aea4baeeebdad06c1995c64f9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Sep 2020 21:54:59 +1200 Subject: [PATCH 846/862] Allow ESPHome to trigger the HA tag scanned event (#40128) --- homeassistant/components/esphome/__init__.py | 10 ++++++++++ homeassistant/components/esphome/manifest.json | 4 +--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index f1b22c13bf1..497dc0deda8 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -129,6 +129,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool "Can only generate events under esphome domain! (%s)", host ) return + + # Call native tag scan + if service_name == "tag_scanned": + tag_id = service_data["tag_id"] + device_id = service_data["device_id"] + hass.async_create_task( + hass.components.tag.async_scan_tag(tag_id, device_id) + ) + return + hass.bus.async_fire(service.service, service_data) else: hass.async_create_task( diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c57ff4a5520..123c7931e41 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -6,7 +6,5 @@ "requirements": ["aioesphomeapi==2.6.3"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], - "after_dependencies": [ - "zeroconf" - ] + "after_dependencies": ["zeroconf", "tag"] } From abca1778948412171688137eff729ae326ae74f6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Sep 2020 22:22:06 +1200 Subject: [PATCH 847/862] Use device name stored in device_info for tag scan in ESPHome (#40130) --- homeassistant/components/esphome/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 497dc0deda8..64aae3ce522 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -133,9 +133,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool # Call native tag scan if service_name == "tag_scanned": tag_id = service_data["tag_id"] - device_id = service_data["device_id"] hass.async_create_task( - hass.components.tag.async_scan_tag(tag_id, device_id) + hass.components.tag.async_scan_tag( + tag_id, entry_data.device_info.name + ) ) return From 8dde59be02217113fe715a28f1a37a8163ca8125 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 12:48:38 +0200 Subject: [PATCH 848/862] Guard for when Yandex Transport data fetching fails (#40131) --- homeassistant/components/yandex_transport/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index cde115cb12f..957844e519d 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -60,7 +60,7 @@ class DiscoverYandexTransport(Entity): self._name = name self._attrs = None - async def async_update(self): + async def async_update(self, *, tries=0): """Get the latest data from maps.yandex.ru and update the states.""" attrs = {} closer_time = None @@ -73,8 +73,12 @@ class DiscoverYandexTransport(Entity): key_error, yandex_reply, ) + if tries > 0: + return await self.requester.set_new_session() - data = (await self.requester.get_stop_info(self._stop_id))["data"] + await self.async_update(tries=tries + 1) + return + stop_name = data["name"] transport_list = data["transports"] for transport in transport_list: From b28dbe20b696d5c25ce6e7216425aa8953f20919 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 13:17:05 +0200 Subject: [PATCH 849/862] Fix ESPHome scan tag device ID (#40132) --- homeassistant/components/esphome/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 64aae3ce522..c9d07a22ec6 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -66,6 +66,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] password = entry.data[CONF_PASSWORD] + device_id = None zeroconf_instance = await zeroconf.async_get_instance(hass) @@ -134,9 +135,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if service_name == "tag_scanned": tag_id = service_data["tag_id"] hass.async_create_task( - hass.components.tag.async_scan_tag( - tag_id, entry_data.device_info.name - ) + hass.components.tag.async_scan_tag(tag_id, device_id) ) return @@ -177,10 +176,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool async def on_login() -> None: """Subscribe to states and list entities on successful API login.""" + nonlocal device_id try: entry_data.device_info = await cli.device_info() entry_data.available = True - await _async_setup_device_registry(hass, entry, entry_data.device_info) + device_id = await _async_setup_device_registry( + hass, entry, entry_data.device_info + ) entry_data.async_update_device_state(hass) entity_infos, services = await cli.list_entities_services() @@ -276,7 +278,7 @@ async def _async_setup_device_registry( if device_info.compilation_time: sw_version += f" ({device_info.compilation_time})" device_registry = await dr.async_get_registry(hass) - device_registry.async_get_or_create( + entry = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, name=device_info.name, @@ -284,6 +286,7 @@ async def _async_setup_device_registry( model=device_info.model, sw_version=sw_version, ) + return entry.id async def _register_service( From f1169120ae043bf687313739f49397492523f54a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 13:37:51 +0000 Subject: [PATCH 850/862] Bumped version to 0.115.0b11 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ec1f366fabe..33e059b5846 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b10" +PATCH_VERSION = "0b11" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From c62a6cd779d8ae9c02fb1567d9fd54a98295e845 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 16:26:34 +0200 Subject: [PATCH 851/862] Fix scene validator (#40140) --- homeassistant/components/homeassistant/scene.py | 8 ++++---- tests/components/homeassistant/test_scene.py | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 7e4a0433344..1ff3915f121 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -35,15 +35,15 @@ def _convert_states(states): """Convert state definitions to State objects.""" result = {} - for entity_id in states: + for entity_id, info in states.items(): entity_id = cv.entity_id(entity_id) - if isinstance(states[entity_id], dict): - entity_attrs = states[entity_id].copy() + if isinstance(info, dict): + entity_attrs = info.copy() state = entity_attrs.pop(ATTR_STATE, None) attributes = entity_attrs else: - state = states[entity_id] + state = info attributes = {} # YAML translates 'on' to a boolean diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index a1f502a3475..8f47d891f9f 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -320,3 +320,12 @@ async def test_config(hass): no_icon = hass.states.get("scene.scene_no_icon") assert no_icon is not None assert "icon" not in no_icon.attributes + + +def test_validator(): + """Test validators.""" + parsed = ha_scene.STATES_SCHEMA({"light.Test": {"state": "on"}}) + assert len(parsed) == 1 + assert "light.test" in parsed + assert parsed["light.test"].entity_id == "light.test" + assert parsed["light.test"].state == "on" From d3a59652bb8502b25698ff28e6fb048b1f665eed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 17:04:57 +0200 Subject: [PATCH 852/862] Fix missing f from f-strings in cast integration (#40144) --- homeassistant/components/cast/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 177babdb476..f948c51655b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -375,9 +375,9 @@ class CastDevice(MediaPlayerEntity): if tts_base_url and media_status.content_id.startswith(tts_base_url): url_description = f" from tts.base_url ({tts_base_url})" if external_url and media_status.content_id.startswith(external_url): - url_description = " from external_url ({external_url})" + url_description = f" from external_url ({external_url})" if internal_url and media_status.content_id.startswith(internal_url): - url_description = " from internal_url ({internal_url})" + url_description = f" from internal_url ({internal_url})" _LOGGER.error( "Failed to cast media %s%s. Please make sure the URL is: " From 9ffcf35b23bc0707975db9042404c40f49319b54 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Sep 2020 21:38:40 +0200 Subject: [PATCH 853/862] Fix local media browser source conflicting with local www folder (#40151) --- .../components/media_source/local_source.py | 8 +++---- homeassistant/config.py | 4 ++-- tests/common.py | 2 +- tests/components/media_source/test_init.py | 6 ++--- .../media_source/test_local_source.py | 24 +++++++++---------- tests/test_config.py | 2 +- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index e14735fb60b..6c60da562e0 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -64,7 +64,7 @@ class LocalSource(MediaSource): mime_type, _ = mimetypes.guess_type( str(self.async_full_path(source_dir_id, location)) ) - return PlayMedia(f"/local_source/{item.identifier}", mime_type) + return PlayMedia(f"/media/{item.identifier}", mime_type) async def async_browse_media( self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES @@ -177,7 +177,7 @@ class LocalMediaView(HomeAssistantView): Returns media files in config/media. """ - url = "/local_source/{source_dir_id}/{location:.*}" + url = "/media/{source_dir_id}/{location:.*}" name = "media" def __init__(self, hass: HomeAssistant, source: LocalSource): @@ -190,10 +190,10 @@ class LocalMediaView(HomeAssistantView): ) -> web.FileResponse: """Start a GET request.""" if location != sanitize_path(location): - return web.HTTPNotFound() + raise web.HTTPNotFound() if source_dir_id not in self.hass.config.media_dirs: - return web.HTTPNotFound() + raise web.HTTPNotFound() media_path = self.source.async_full_path(source_dir_id, location) diff --git a/homeassistant/config.py b/homeassistant/config.py index 3d6e3fb041c..8f9dc7c3d62 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -510,9 +510,9 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non if CONF_MEDIA_DIRS not in config: if is_docker_env(): - hac.media_dirs = {"media": "/media"} + hac.media_dirs = {"local": "/media"} else: - hac.media_dirs = {"media": hass.config.path("media")} + hac.media_dirs = {"local": hass.config.path("media")} # Init whitelist external dir hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()} diff --git a/tests/common.py b/tests/common.py index b36439d4110..d516873786b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -205,7 +205,7 @@ async def async_test_home_assistant(loop): hass.config.elevation = 0 hass.config.time_zone = date_util.get_time_zone("US/Pacific") hass.config.units = METRIC_SYSTEM - hass.config.media_dirs = {"media": get_test_config_dir("media")} + hass.config.media_dirs = {"local": get_test_config_dir("media")} hass.config.skip_pip = True hass.config_entries = config_entries.ConfigEntries(hass, {}) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index c7fc2dd6338..a891fb0d11d 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -65,7 +65,7 @@ async def test_async_resolve_media(hass): media = await media_source.async_resolve_media( hass, - media_source.generate_media_source_id(const.DOMAIN, "media/test.mp3"), + media_source.generate_media_source_id(const.DOMAIN, "local/test.mp3"), ) assert isinstance(media, media_source.models.PlayMedia) @@ -140,7 +140,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client): client = await hass_ws_client(hass) - media = media_source.models.PlayMedia("/local_source/media/test.mp3", "audio/mpeg") + media = media_source.models.PlayMedia("/media/local/test.mp3", "audio/mpeg") with patch( "homeassistant.components.media_source.async_resolve_media", @@ -150,7 +150,7 @@ async def test_websocket_resolve_media(hass, hass_ws_client): { "id": 1, "type": "media_source/resolve_media", - "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3", + "media_content_id": f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3", } ) diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index bd0a1435eef..e3e2a3f1617 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -11,7 +11,7 @@ async def test_async_browse_media(hass): """Test browse media.""" local_media = hass.config.path("media") await async_process_ha_core_config( - hass, {"media_dirs": {"media": local_media, "recordings": local_media}} + hass, {"media_dirs": {"local": local_media, "recordings": local_media}} ) await hass.async_block_till_done() @@ -21,14 +21,14 @@ async def test_async_browse_media(hass): # Test path not exists with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test/not/exist" + hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test/not/exist" ) assert str(excinfo.value) == "Path does not exist." # Test browse file with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/test.mp3" + hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test.mp3" ) assert str(excinfo.value) == "Path is not a directory." @@ -42,7 +42,7 @@ async def test_async_browse_media(hass): # Test directory traversal with pytest.raises(media_source.BrowseError) as excinfo: await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/../configuration.yaml" + hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/../configuration.yaml" ) assert str(excinfo.value) == "Invalid path." @@ -53,7 +53,7 @@ async def test_async_browse_media(hass): assert media media = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{const.DOMAIN}/media/." + hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/." ) assert media @@ -67,7 +67,7 @@ async def test_media_view(hass, hass_client): """Test media view.""" local_media = hass.config.path("media") await async_process_ha_core_config( - hass, {"media_dirs": {"media": local_media, "recordings": local_media}} + hass, {"media_dirs": {"local": local_media, "recordings": local_media}} ) await hass.async_block_till_done() @@ -77,23 +77,23 @@ async def test_media_view(hass, hass_client): client = await hass_client() # Protects against non-existent files - resp = await client.get("/local_source/media/invalid.txt") + resp = await client.get("/media/local/invalid.txt") assert resp.status == 404 - resp = await client.get("/local_source/recordings/invalid.txt") + resp = await client.get("/media/recordings/invalid.txt") assert resp.status == 404 # Protects against non-media files - resp = await client.get("/local_source/media/not_media.txt") + resp = await client.get("/media/local/not_media.txt") assert resp.status == 404 # Protects against unknown local media sources - resp = await client.get("/local_source/unknown_source/not_media.txt") + resp = await client.get("/media/unknown_source/not_media.txt") assert resp.status == 404 # Fetch available media - resp = await client.get("/local_source/media/test.mp3") + resp = await client.get("/media/local/test.mp3") assert resp.status == 200 - resp = await client.get("/local_source/recordings/test.mp3") + resp = await client.get("/media/recordings/test.mp3") assert resp.status == 200 diff --git a/tests/test_config.py b/tests/test_config.py index a6c6ee86acc..fb22ee1118e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -499,7 +499,7 @@ async def test_loading_configuration_default_media_dirs_docker(hass): assert hass.config.location_name == "Huis" assert len(hass.config.allowlist_external_dirs) == 2 assert "/media" in hass.config.allowlist_external_dirs - assert hass.config.media_dirs == {"media": "/media"} + assert hass.config.media_dirs == {"local": "/media"} async def test_loading_configuration_from_packages(hass): From f7d7765d5edfb8ed225e8838817e3173ab6c3229 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 16 Sep 2020 22:31:43 +0200 Subject: [PATCH 854/862] Update frontend to 20200916.0 (#40153) --- 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 9b499f0376a..b7bb77346de 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200915.0"], + "requirements": ["home-assistant-frontend==20200916.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ccede9062a0..a535d83d39c 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.37.0 -home-assistant-frontend==20200915.0 +home-assistant-frontend==20200916.0 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5a9f88c4963..87504a86112 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200915.0 +home-assistant-frontend==20200916.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 95fb575eee8..7b774ab7a5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200915.0 +home-assistant-frontend==20200916.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From a9e220c96b17a34b293b92ce30499831c970cb47 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 Sep 2020 20:50:22 +0000 Subject: [PATCH 855/862] Bumped version to 0.115.0b12 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 33e059b5846..e01c5335757 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b11" +PATCH_VERSION = "0b12" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) From 99a57f5a4e245fd737141348657605ada3c4927b Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 17 Sep 2020 09:01:28 -0500 Subject: [PATCH 856/862] Check mpd time type before splitting it (#40139) --- homeassistant/components/mpd/media_player.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 8a46cef6eb3..845b0ae506b 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -138,12 +138,12 @@ class MpdDevice(MediaPlayerEntity): if position is None: position = self._status.get("time") - if position is not None and ":" in position: + if isinstance(position, str) and ":" in position: position = position.split(":")[0] if position is not None and self._media_position != position: self._media_position_updated_at = dt_util.utcnow() - self._media_position = int(position) + self._media_position = int(float(position)) self._update_playlists() @@ -159,8 +159,9 @@ class MpdDevice(MediaPlayerEntity): self._connect() self._fetch_status() - except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError): + except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error: # Cleanly disconnect in case connection is not in valid state + _LOGGER.debug("Error updating status: %s", error) self._disconnect() @property From 78022bf145fec02d50f2bc40b57c11c667f54b6a Mon Sep 17 00:00:00 2001 From: cagnulein Date: Thu, 17 Sep 2020 11:35:13 +0200 Subject: [PATCH 857/862] Fix luci device_tracker not reliably reporting home/away state (#40160) --- homeassistant/components/luci/device_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 9d71b3d263a..fe64c90bf4c 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -94,6 +94,7 @@ class LuciDeviceScanner(DeviceScanner): last_results = [] for device in result: - last_results.append(device) + if device.reachable: + last_results.append(device) self.last_results = last_results From 5271a3eb1e9bbbc693d082cfd6fe6e37404c21c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 17 Sep 2020 12:08:09 +0200 Subject: [PATCH 858/862] Update pyhaversion to 3.4.2 (#40161) --- homeassistant/components/version/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 1f07c757ad8..7fc1a097d81 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,7 +2,7 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==3.4.0"], + "requirements": ["pyhaversion==3.4.2"], "codeowners": ["@fabaff", "@ludeeus"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 87504a86112..e34f48898fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1377,7 +1377,7 @@ pygtfs==0.1.5 pygti==0.6.0 # homeassistant.components.version -pyhaversion==3.4.0 +pyhaversion==3.4.2 # homeassistant.components.heos pyheos==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b774ab7a5a..c1b419263e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -662,7 +662,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.6.0 # homeassistant.components.version -pyhaversion==3.4.0 +pyhaversion==3.4.2 # homeassistant.components.heos pyheos==0.6.0 From 13cfd1bae1f80be1c0100e532cce173eedbcd19c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 17 Sep 2020 14:44:19 +0200 Subject: [PATCH 859/862] Updated frontend to 20200917.1 (#40170) --- 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 b7bb77346de..96d2a51cb09 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200916.0"], + "requirements": ["home-assistant-frontend==20200917.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a535d83d39c..e5978245bac 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.37.0 -home-assistant-frontend==20200916.0 +home-assistant-frontend==20200917.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index e34f48898fb..7ff2d0695b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200916.0 +home-assistant-frontend==20200917.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1b419263e5..d492e3acbb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -370,7 +370,7 @@ hole==0.5.1 holidays==0.10.3 # homeassistant.components.frontend -home-assistant-frontend==20200916.0 +home-assistant-frontend==20200917.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 6f26722f692691b6fd480bb36ea6c7ca778cdae8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 17 Sep 2020 16:45:55 +0200 Subject: [PATCH 860/862] Fix editing tags only get isoformat from datetime (#40174) Co-authored-by: Paulus Schoutsen --- homeassistant/components/tag/__init__.py | 3 ++- tests/components/tag/test_init.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 0b445fbf575..321dce9a296 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -82,7 +82,7 @@ class TagStorageCollection(collection.StorageCollection): """Return a new updated data object.""" data = {**data, **self.UPDATE_SCHEMA(update_data)} # make last_scanned JSON serializeable - if LAST_SCANNED in data: + if LAST_SCANNED in update_data: data[LAST_SCANNED] = data[LAST_SCANNED].isoformat() return data @@ -100,6 +100,7 @@ async def async_setup(hass: HomeAssistant, config: dict): collection.StorageCollectionWebsocket( storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) + return True diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index e4b810e0661..ecb927c1c5c 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -48,6 +48,30 @@ async def test_ws_list(hass, hass_ws_client, storage_setup): assert "test tag" in result +async def test_ws_update(hass, hass_ws_client, storage_setup): + """Test listing tags via WS.""" + assert await storage_setup() + await async_scan_tag(hass, "test tag", "some_scanner") + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": "test tag", + "name": "New name", + } + ) + resp = await client.receive_json() + assert resp["success"] + + item = resp["result"] + + assert item["id"] == "test tag" + assert item["name"] == "New name" + + async def test_tag_scanned(hass, hass_ws_client, storage_setup): """Test scanning tags.""" assert await storage_setup() From f5aee6b886e9f52abdcd95a5eb877b754a7ec624 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2020 09:47:23 -0500 Subject: [PATCH 861/862] Add missing conext preservation to bayesian and universal (#40178) We already do this for template sensors, but it was missing for bayesian and universal --- .../components/bayesian/binary_sensor.py | 3 + .../components/universal/media_player.py | 7 +- .../components/bayesian/test_binary_sensor.py | 85 ++++++++++++++++++- .../components/universal/test_media_player.py | 11 ++- 4 files changed, 103 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 90540e456c5..4768b3f4fe6 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -182,6 +182,7 @@ class BayesianBinarySensor(BinarySensorEntity): entity = event.data.get("entity_id") self.current_observations.update(self._record_entity_observations(entity)) + self.async_set_context(event.context) self._recalculate_and_write_state() self.async_on_remove( @@ -220,6 +221,8 @@ class BayesianBinarySensor(BinarySensorEntity): obs_entry = None self.current_observations[obs["id"]] = obs_entry + if event: + self.async_set_context(event.context) self._recalculate_and_write_state() for template in self.observations_by_template: diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index c38afc139cf..aedc27c2a29 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -145,8 +145,9 @@ class UniversalMediaPlayer(MediaPlayerEntity): """Subscribe to children and template state changes.""" @callback - def _async_on_dependency_update(*_): + def _async_on_dependency_update(event): """Update ha state when dependencies update.""" + self.async_set_context(event.context) self.async_schedule_update_ha_state(True) @callback @@ -158,6 +159,10 @@ class UniversalMediaPlayer(MediaPlayerEntity): self._state_template_result = None else: self._state_template_result = result + + if event: + self.async_set_context(event.context) + self.async_schedule_update_ha_state(True) if self._state_template is not None: diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 57c7f404c7f..5755a7e24e9 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -9,7 +9,14 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_RELOAD, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_RELOAD, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, +) +from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component, setup_component from tests.async_mock import patch @@ -686,3 +693,79 @@ async def test_reload(hass): def _get_fixtures_base_path(): return path.dirname(path.dirname(path.dirname(__file__))) + + +async def test_template_triggers(hass): + """Test sensor with template triggers.""" + hass.states.async_set("input_boolean.test", STATE_OFF) + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "template", + "value_template": "{{ states.input_boolean.test.state }}", + "prob_given_true": 1999.9, + }, + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_binary").state == STATE_OFF + + events = [] + hass.helpers.event.async_track_state_change_event( + "binary_sensor.test_binary", callback(lambda event: events.append(event)) + ) + + context = Context() + hass.states.async_set("input_boolean.test", STATE_ON, context=context) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert events[0].context == context + + +async def test_state_triggers(hass): + """Test sensor with state triggers.""" + hass.states.async_set("sensor.test_monitored", STATE_OFF) + + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "state", + "entity_id": "sensor.test_monitored", + "to_state": "off", + "prob_given_true": 999.9, + "prob_given_false": 999.4, + }, + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_binary").state == STATE_OFF + + events = [] + hass.helpers.event.async_track_state_change_event( + "binary_sensor.test_binary", callback(lambda event: events.append(event)) + ) + + context = Context() + hass.states.async_set("sensor.test_monitored", STATE_ON, context=context) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert events[0].context == context diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 38949c32ebb..76a397496ad 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -20,6 +20,7 @@ from homeassistant.const import ( STATE_PLAYING, STATE_UNKNOWN, ) +from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component from tests.async_mock import patch @@ -812,10 +813,18 @@ async def test_master_state_with_template(hass): await hass.async_block_till_done() hass.states.get("media_player.tv").state == STATE_ON - hass.states.async_set("input_boolean.test", STATE_ON) + events = [] + + hass.helpers.event.async_track_state_change_event( + "media_player.tv", callback(lambda event: events.append(event)) + ) + + context = Context() + hass.states.async_set("input_boolean.test", STATE_ON, context=context) await hass.async_block_till_done() hass.states.get("media_player.tv").state == STATE_OFF + assert events[0].context == context async def test_reload(hass): From f77b3d4714faec0196bb3ceae780111fdc2b6115 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 17 Sep 2020 17:10:26 +0200 Subject: [PATCH 862/862] Bumped version to 0.115.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e01c5335757..80129e097d0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 115 -PATCH_VERSION = "0b12" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1)